Skip to content

Feat/noid/hr sick leave overview - #27

Merged
karlitschek merged 3 commits into
mainfrom
feat/noid/hr-sick-leave-overview
Aug 7, 2026
Merged

Feat/noid/hr sick leave overview#27
karlitschek merged 3 commits into
mainfrom
feat/noid/hr-sick-leave-overview

Conversation

@karlitschek

Copy link
Copy Markdown
Member

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

Frank Karlitschek and others added 3 commits August 6, 2026 20:54
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>
@nextcloud-command nextcloud-command added the AI assisted This PR contains AI-assisted commits label Aug 7, 2026
@karlitschek
karlitschek merged commit 1a937c2 into main Aug 7, 2026
25 of 31 checks passed
@karlitschek
karlitschek deleted the feat/noid/hr-sick-leave-overview branch August 7, 2026 10:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI assisted This PR contains AI-assisted commits

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants