Skip to content

feat(hr): let HR correct and cancel recorded absences - #34

Merged
karlitschek merged 1 commit into
mainfrom
feat/hr-edit-cancel-absences
Aug 10, 2026
Merged

feat(hr): let HR correct and cancel recorded absences#34
karlitschek merged 1 commit into
mainfrom
feat/hr-edit-cancel-absences

Conversation

@karlitschek

Copy link
Copy Markdown
Member

HR could record an absence but never revisit one. The permission layer already allowed it — canModify returns true for HR on any request, and RequestService's cancel has an HR override that skips the withdrawal step — and the sidebar already renders Edit and Cancel behind canModify. What was missing was any screen on which an individual record is reachable: My leave shows only your own, Approvals only what is pending, and Who's off was a read-only calendar.

So this adds the reaching, not new permissions.

Absences (#/hr/absences) is the counterpart to Record absence and sits next to it in the navigation, because recording and correcting are the same job at different times. It lists every record with filters for employee, leave type, status and year, and opens the standard sidebar — so Edit and Cancel come from the code that already implements them, rather than a second path to the same API with its own bugs.

Cancel, not delete. A wrong entry is cancelled, leaving the row and its history intact for the audit trail (§17); nothing in the UI hard-deletes leave. Since HR's cancel is immediate and notifies the employee, and is now reached from a browse list where a misclick is easy, it is confirmed first. The button also stops claiming "Request withdrawal" to an HR user, which was never what the backend did for them.

Two smaller reach improvements, since a list you have to think to visit is not where a wrong sick day gets noticed:

  • Pills in HR's Who's off timeline open their request. Only there — the calendar already carries requestId, but a manager's team timeline can include leave whose detail the API would refuse, so the behaviour is opt-in per view rather than on by default.
  • The Sick leave overview drills down into one employee's individual records for that year, filtered to the sick type when the report aggregated only one.

Also fixed, both surfaced by the above:

  • HR editing an HR-recorded absence got a type picker that did not contain the record's own type, so sick leave could not be chosen again once changed.
  • findFiltered ordered by start_date alone, which is not unique. That was harmless until this view introduced limit/offset paging, where a non-total order can repeat or skip rows; an id tiebreaker makes it deterministic.

No new endpoint, no permission change: scope=hr already accepted every filter this view needs.

🤖 AI (if applicable)

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

HR could record an absence but never revisit one. The permission layer already
allowed it — canModify returns true for HR on any request, and RequestService's
cancel has an HR override that skips the withdrawal step — and the sidebar
already renders Edit and Cancel behind canModify. What was missing was any
screen on which an individual record is reachable: My leave shows only your
own, Approvals only what is pending, and Who's off was a read-only calendar.

So this adds the reaching, not new permissions.

Absences (#/hr/absences) is the counterpart to Record absence and sits next to
it in the navigation, because recording and correcting are the same job at
different times. It lists every record with filters for employee, leave type,
status and year, and opens the standard sidebar — so Edit and Cancel come from
the code that already implements them, rather than a second path to the same
API with its own bugs.

Cancel, not delete. A wrong entry is cancelled, leaving the row and its history
intact for the audit trail (§17); nothing in the UI hard-deletes leave. Since
HR's cancel is immediate and notifies the employee, and is now reached from a
browse list where a misclick is easy, it is confirmed first. The button also
stops claiming "Request withdrawal" to an HR user, which was never what the
backend did for them.

Two smaller reach improvements, since a list you have to think to visit is not
where a wrong sick day gets noticed:

* Pills in HR's Who's off timeline open their request. Only there — the
  calendar already carries requestId, but a manager's team timeline can include
  leave whose detail the API would refuse, so the behaviour is opt-in per view
  rather than on by default.
* The Sick leave overview drills down into one employee's individual records
  for that year, filtered to the sick type when the report aggregated only one.

Also fixed, both surfaced by the above:

* HR editing an HR-recorded absence got a type picker that did not contain the
  record's own type, so sick leave could not be chosen again once changed.
* findFiltered ordered by start_date alone, which is not unique. That was
  harmless until this view introduced limit/offset paging, where a non-total
  order can repeat or skip rows; an id tiebreaker makes it deterministic.

No new endpoint, no permission change: scope=hr already accepted every filter
this view needs.

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 10, 2026
@karlitschek
karlitschek requested a lite review from Copilot August 10, 2026 13:19
@karlitschek
karlitschek merged commit 58596a3 into main Aug 10, 2026
28 of 31 checks passed
@karlitschek
karlitschek deleted the feat/hr-edit-cancel-absences branch August 10, 2026 13:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Adds an HR-facing “Absences” browse screen so HR can reach any recorded absence and reuse the existing sidebar edit/cancel flows (including a confirmation for HR’s immediate cancel), plus a few drill-in entry points and deterministic paging order.

Changes:

  • Add new #/hr/absences view with filtering + paging and deep-linkable query params.
  • Make HR “Who’s off” timeline pills selectable, and add sick-leave report drilldown into the absences list.
  • Improve correctness/UX around editing/cancelling (type picker for HR edits, confirm HR cancel) and make DB ordering deterministic for offset paging.

Reviewed changes

Copilot reviewed 11 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/views/hr/HrWhosOff.vue Makes timeline pills selectable for HR and adds a hint + store wiring.
src/views/hr/HrSickLeave.vue Adds drilldown navigation from sick-leave aggregates into HR absences list (with keyboard-reachable control).
src/views/hr/HrAbsences.vue New HR absence browse/filter/paging list that opens the standard request sidebar.
src/router.js Registers the new hr-absences route.
src/components/TeamTimeline.vue Adds optional “selectable pills” mode emitting select events and styling active state.
src/components/RequestSidebar.vue Adds HR cancellation confirmation + corrects cancel label logic for HR override.
src/components/RequestDialog.vue Fixes leave-type options for HR editing existing records (includes non-requestable types).
src/App.vue Adds nav item + provides absence:openRecord event to open the record dialog.
lib/Db/LeaveRequestMapper.php Makes ordering total with id tiebreaker to prevent paging duplicates/skips.
SPECIFICATION.md Documents the new Absences view and selectable timeline behavior.
README.md Updates HR area description and adds Absences screenshot narrative.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +305 to +308
hrOverride() {
return !!store.session.isHr
&& (this.detail.employeeUid !== store.session.uid || store.isHrRecorded(this.detail))
},
Comment on lines +162 to +168
const q = this.$route?.query || {}
const thisYear = new Date().getFullYear()
const employee = q.employee
? { uid: q.employee, displayName: q.employeeName || q.employee }
: null
const year = q.year ? Number(q.year) : thisYear
return {
Comment on lines +285 to +297
async reload() {
this.loading = true
try {
this.rows = await this.fetch(0)
this.hasMore = this.rows.length === PAGE_SIZE
} catch {
this.rows = []
this.hasMore = false
showError(t('absence', 'Could not load the absences'))
} finally {
this.loading = false
}
},
Comment on lines +271 to +283
async onEmployeeSearch(query) {
if (!query || query.length < 2) {
return
}
this.employeeLoading = true
try {
this.employeeOptions = await api.searchUsers(query)
} catch {
this.employeeOptions = []
} finally {
this.employeeLoading = false
}
},
Comment on lines +85 to +90
<tr
v-for="(row, index) in filtered"
:key="row.employeeUid"
class="row"
:class="{ 'row--drillable': row.days > 0 }"
@click="openRecords(row)">
Comment on lines +64 to 68
<component
:is="selectable ? 'button' : 'span'"
v-for="(seg, i) in row.segments"
:key="i"
class="gantt__pill"
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.

3 participants