Skip to content

Develop - #90

Merged
bladehell-ai merged 2 commits into
mainfrom
develop
Aug 21, 2026
Merged

Develop#90
bladehell-ai merged 2 commits into
mainfrom
develop

Conversation

@bladehell-ai

Copy link
Copy Markdown
Collaborator

No description provided.

bladehell-ai and others added 2 commits August 21, 2026 19:59
The same date range produced four different answers. Measured org-wide over one
60-day window: reports tab 23.02h, report builder 31.52h, dashboard 31.51h, Time
page 31.68h — of which the Time page displayed only the slice on screen (3905h
against a true 2001h across the full dataset).

There was no shared definition of "worked time", and the four surfaces disagreed
on all three axes at once:

* Which rows count — analytics filtered `type='tracked'` for its headline KPI,
  dropping approved MANUAL time, while its own charts and log tables filtered
  nothing and counted IDLE. A table never reconciled with its own header.
* How long a row is — the dashboard summed `duration_seconds`, a desktop-written
  column that is not trustworthy: idle rows store 1897h against 55h of real
  wall-clock, and several tracked rows store NEGATIVE seconds, which SUM()
  silently subtracts.
* Which day it lands on — summary() grouped by `DATE(started_at)`, i.e. the UTC
  date, inside range bounds resolved in the org timezone. For Asia/Karachi that
  pushed work done before 05:00 local onto the previous day's bar.

And the Time page summed the page it had been handed (20 rows) beside an entry
count covering every match, so the card moved as you paged.

App\Support\WorkedTime is now the single definition — clamped timestamp-derived
duration, org-timezone day bucket, approved-non-idle scope — wired through
ReportService, DashboardController, and TimeEntryController, which now returns
total_seconds/idle_seconds/unapproved_seconds for the WHOLE filtered set instead
of letting the client sum a page.

fix(reports): honour the report user filter by permission scope, not role name

Picking any user in the Advanced Report Builder — or "All Users" — returned the
logged-in user's own figures.

`reports.view` is granted to the employee role at scope 'own', so an employee
legitimately reaches these endpoints. The web client gated the user picker on
merely HOLDING the permission, so it rendered a full team dropdown, sent user_id,
and the API then clamped every request back to the caller. The UI advertised a
capability the API was never going to honour.

The server side was brittle too: it decided visibility with `$user->isEmployee()`,
which reads the `users.role` STRING — a fallback used only when a user has no
user_roles row, on a role set that is open because orgs define custom roles.

ReportController now resolves visibility from the SCOPE on the actor's grant:
organization sees anyone, project sees their team (a requested outsider narrows
to self rather than widening), own is clamped to self. ReportService accepts a
list of user ids so project scope is expressible instead of falling through to
the whole org. The reports page gates the picker on
hasPermissionWithScope('reports.view','project') — the same test the Time page
already uses — so own-scoped users get no dropdown at all.

Verified with real dispatched requests: owner sees 15.58h all / 13.71h for a
chosen member; an own-scoped employee gets their own 1.87h whatever they ask.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t-user-filter

fix(reports): one worked-time total across every surface, and a user filter that actually filters
@bladehell-ai
bladehell-ai merged commit 6490919 into main Aug 21, 2026
9 checks passed
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