Feat/noid/hr sick leave overview - #27
Merged
Merged
Conversation
Nextcloud calls date_default_timezone_set('UTC') for the whole request
(lib/OC.php), so a bare date('Y-m-d') answers in UTC wherever the caller is.
For a stored timestamp that is correct. For a day boundary compared against a
date somebody typed it is not.
At 09:00 on 2 January in Auckland it is still 1 January in UTC, so an employee
was told their leave "is entirely in the past" for a day that had not finished
where they live — and a request made in those hours was attributed to the
previous year's entitlement. Berlin has the same problem in the last hour of
each day.
ClockService now owns the question, with the audience in the method name so
call sites cannot be ambiguous:
* userToday() / userYear() resolve through IDateTimeZone::getTimeZone(), i.e.
the employee's own timezone, and are used for everything a user is judged
against: the past-date check on a new request, the year a balance is shown
for, the dashboard's upcoming-leave cut-off, and the default year for HR
reports and exports.
* serverToday() / serverYear() resolve through getDefaultTimeZone() and are
used where there is no user to ask: the year-rollover job, carry-over expiry
(company policy, only ever run from that job), and the reminder and
escalation jobs.
The instant comes from ITimeFactory, so the boundary logic is testable.
The reminder and escalation jobs were already passing an explicit UTC
timezone, which is the same thing PHP's default gave them — so this is not a
behaviour change for them, only an explicit one. They now say why they use the
server's timezone rather than appearing to have picked UTC at random.
Stored timestamps (created_at, updated_at, decided_at, notification times)
keep using new \DateTime() and stay in UTC. They are instants, not day
boundaries, and rewriting them would change the meaning of existing rows.
Covered by ClockServiceTest, which pins an instant that falls on a different
calendar day either side of UTC and asserts both audiences separately. The
three service tests whose constructors grew a dependency get a clock that
answers exactly what date('Y-m-d') used to, so their existing expectations
keep the same meaning.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
HR could see balances, statistics, who's off and exports, but nothing answered "who has been off sick, and how much". This adds an HR-only view listing every employee ranked by sick days, with the number of separate absences, the longest single absence, and the most recent one. HR only, deliberately. Unlike the coverage and who's-off views, this is health-adjacent data about named people, so the endpoint asserts the HR role and the navigation entry is inside the existing isHr block. Line managers do not get it, even for their own reports. Which types count as sickness is not a flag on the leave type, so the default is the seeded 'sick' key with an optional explicit typeId override. The types actually aggregated are returned with the report, so the UI can name them instead of implying that "sickness" is a fixed concept — and an instance with no matching type gets an explanatory empty state rather than a table of zeroes. Counting rules, chosen to match the rest of the app: * Only approved leave. A pending record is not yet a fact about someone. * Days are attributed to the year the leave starts, as balances are. The range query also returns leave that merely overlaps the year, so anything starting in a different year is filtered out — otherwise leave over New Year would be counted twice. * Employees outside the requested group, or who have left, are ignored rather than appearing with a name the directory can no longer resolve. * Ranked by days descending, ties broken by name so the order does not shuffle between loads. One query for the whole company via findForEmployeesInRange, not one per employee. The view keeps the shape of the other HR tables (search, year picker, avatars, skeleton) and adds a bar scaled to the worst case, so the ranking reads without comparing numbers. It defaults to hiding employees with no sick leave, since on most instances that is the majority of the list. Stacked on the timezone fix: the default year comes from ClockService, so the report opens on the year the HR user is actually in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
HrSickLeave.vue referenced .page, .page__header, .page__tools, .page__search, .table-wrap and .tbl in its template but never defined them in its scoped style block. Those class names are defined in sibling views such as HrBalances.vue, where `scoped` keeps them from applying here, so the page fell back to browser defaults: a vertically stacked toolbar and a table with no cell padding, no row borders and no right-aligned numerics. Add the missing rules, matching the HrBalances conventions. Also move the summary line and the empty state out of the horizontally scrolling container so they no longer scroll with a wide table, and skip the table entirely when there are no rows instead of rendering a bare header above the empty state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 AI (if applicable)