feat(hr): let HR correct and cancel recorded absences - #34
Conversation
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>
There was a problem hiding this comment.
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/absencesview 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.
| hrOverride() { | ||
| return !!store.session.isHr | ||
| && (this.detail.employeeUid !== store.session.uid || store.isHrRecorded(this.detail)) | ||
| }, |
| 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 { |
| 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 | ||
| } | ||
| }, |
| 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 | ||
| } | ||
| }, |
| <tr | ||
| v-for="(row, index) in filtered" | ||
| :key="row.employeeUid" | ||
| class="row" | ||
| :class="{ 'row--drillable': row.days > 0 }" | ||
| @click="openRecords(row)"> |
| <component | ||
| :is="selectable ? 'button' : 'span'" | ||
| v-for="(seg, i) in row.segments" | ||
| :key="i" | ||
| class="gantt__pill" |
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:
Also fixed, both surfaced by the above:
No new endpoint, no permission change: scope=hr already accepted every filter this view needs.
🤖 AI (if applicable)