hr_lite 0.58.0 → 0.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/app/assets/javascripts/hr_lite/crm_picker.js +4 -2
- data/app/assets/javascripts/hr_lite/mention.js +2 -2
- data/app/assets/javascripts/hr_lite/photo_crop.js +4 -0
- data/app/assets/javascripts/hr_lite/search.js +3 -2
- data/app/assets/javascripts/hr_lite/tax_preview.js +3 -2
- data/app/controllers/hr_lite/admin/assets_controller.rb +4 -4
- data/app/controllers/hr_lite/admin/attendances_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/ca_reviews_controller.rb +2 -2
- data/app/controllers/hr_lite/admin/comp_off_requests_controller.rb +4 -4
- data/app/controllers/hr_lite/admin/designation_changes_controller.rb +4 -1
- data/app/controllers/hr_lite/admin/employees_controller.rb +4 -2
- data/app/controllers/hr_lite/admin/expenses_controller.rb +6 -6
- data/app/controllers/hr_lite/admin/hr_requests_controller.rb +4 -4
- data/app/controllers/hr_lite/admin/leave_requests_controller.rb +4 -4
- data/app/controllers/hr_lite/admin/overview_controller.rb +5 -0
- data/app/controllers/hr_lite/admin/regularization_requests_controller.rb +4 -4
- data/app/controllers/hr_lite/admin/resignations_controller.rb +4 -0
- data/app/controllers/hr_lite/admin/tax_declarations_controller.rb +2 -2
- data/app/controllers/hr_lite/admin/wfh_requests_controller.rb +4 -4
- data/app/controllers/hr_lite/admin/work_locations_controller.rb +3 -3
- data/app/controllers/hr_lite/application_controller.rb +5 -0
- data/app/controllers/hr_lite/comp_off_requests_controller.rb +2 -2
- data/app/controllers/hr_lite/hr_requests_controller.rb +2 -2
- data/app/controllers/hr_lite/regularization_requests_controller.rb +2 -2
- data/app/controllers/hr_lite/tax_declarations_controller.rb +2 -2
- data/app/controllers/hr_lite/wfh_requests_controller.rb +2 -2
- data/app/controllers/hr_lite/work_locations_controller.rb +1 -1
- data/app/jobs/hr_lite/calendar_meeting_sync_job.rb +1 -1
- data/app/models/concerns/hr_lite/audited.rb +2 -2
- data/app/models/hr_lite/designation_change.rb +4 -0
- data/app/models/hr_lite/employee_profile.rb +1 -1
- data/app/models/hr_lite/resignation.rb +4 -0
- data/app/models/hr_lite/wfh_request.rb +1 -1
- data/app/views/hr_lite/admin/overview/index.html.erb +17 -0
- data/lib/hr_lite/notifications.rb +5 -5
- data/lib/hr_lite/version.rb +1 -1
- data/lib/hr_lite.rb +11 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 051c3d4efb869bb1c40f819e884e245c5d45462c2664a62464c84ec2686b750e
|
|
4
|
+
data.tar.gz: 263dbc61a8861a69b06b0b6597542e496feac6fd3bb6eb78cadd04a947681f41
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c4b151e5a71926157f4976df9ad3fd55f3b60468c6f0d10f7877d2fc9c2363e1ebe7d5d9a86961660848744a840e9b96f597cc1207d86168f20dd7410096ba60
|
|
7
|
+
data.tar.gz: c457f267dc96c0b89278bf870f431174204cb258879cb68e90b152ef37d224354068732b2b118678e6b1329cf24351c5a26dca8672e120549d83c3b906e622a1
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.60.0] - 2026-10-21
|
|
11
|
+
|
|
12
|
+
Background failures reach a person. No migrations.
|
|
13
|
+
|
|
14
|
+
- **"Failed in the background" card on the HR overview.** A failed event email, leadership email or bell, host notify hook, WFH or work-location notice, audit row, onboarding checklist or calendar read used to write only a log line. Each is now recorded once in the audit trail (`failure.<event>`, with the error class, a 200-character message and the affected user) through `HrLite.record_failure`, and the ten most recent show on the overview to anyone who can open the audit trail. Money-tier subjects stay superadmin-only, as on the audit trail.
|
|
15
|
+
|
|
16
|
+
## [0.59.0] - 2026-10-21
|
|
17
|
+
|
|
18
|
+
No silent failures where a person is waiting on the answer. No migrations. Audit: `docs/audits/silent_failures.md`.
|
|
19
|
+
|
|
20
|
+
- **Decisions say why they failed.** 24 admin and employee actions answered every failed save with a stock line such as "Only pending requests can be decided." They now show the record's own reason, and keep the stock line for the genuine not-pending case.
|
|
21
|
+
- **Offboarding no longer claims access was revoked when it was not.** A failing `offboard_user` hook (offboard, or accepting a resignation on its last day) or `on_designation_change` hook now shows an alert with the cause and what to do by hand.
|
|
22
|
+
- **Search, live tax, lead search and @mentions say when they could not load.** They no longer look like "no results" or show a stale figure. An unreadable ID-card photo (HEIC on desktop) now says so.
|
|
23
|
+
|
|
10
24
|
## [0.58.0] - 2026-10-21
|
|
11
25
|
|
|
12
26
|
Leave and birthdays for the whole team, Keka-style. No migrations. Hosts schedule `HrLite::TeamNoticesJob` daily at 10am.
|
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
if (q.length < 2) return;
|
|
13
13
|
timer = setTimeout(function () {
|
|
14
14
|
fetch(box.dataset.hrlCrm + "?q=" + encodeURIComponent(q), { headers: { Accept: "application/json" }, credentials: "same-origin" })
|
|
15
|
-
.then(function (r) {
|
|
15
|
+
.then(function (r) { if (!r.ok) throw r; return r.json(); })
|
|
16
16
|
.then(function (leads) {
|
|
17
17
|
select.length = 1;
|
|
18
|
+
select.options[0].text = "No lead";
|
|
18
19
|
leads.forEach(function (lead) { select.add(new Option(lead.label, lead.id)); });
|
|
19
20
|
if (leads.length) select.selectedIndex = 1;
|
|
20
|
-
})
|
|
21
|
+
})
|
|
22
|
+
.catch(function () { select.length = 1; select.options[0].text = "Lead search failed. Type again to retry."; });
|
|
21
23
|
}, 250);
|
|
22
24
|
});
|
|
23
25
|
}
|
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
|
|
60
60
|
var lookup = debounce(function (query) {
|
|
61
61
|
fetch(url + "?q=" + encodeURIComponent(query), { headers: { Accept: "application/json" } })
|
|
62
|
-
.then(function (r) {
|
|
62
|
+
.then(function (r) { if (!r.ok) throw r; return r.json(); })
|
|
63
63
|
.then(renderResults)
|
|
64
|
-
.catch(close);
|
|
64
|
+
.catch(function () { close(); menu.textContent = "Could not load names. Keep typing to retry."; menu.hidden = false; });
|
|
65
65
|
}, 200);
|
|
66
66
|
|
|
67
67
|
textarea.addEventListener("input", function () {
|
|
@@ -31,8 +31,9 @@
|
|
|
31
31
|
clearTimeout(timer);
|
|
32
32
|
timer = setTimeout(function () {
|
|
33
33
|
fetch(form.action + "?frame=1&q=" + encodeURIComponent(input.value), { credentials: "same-origin", cache: "no-store" })
|
|
34
|
-
.then(function (response) {
|
|
35
|
-
.then(function (html) { out.innerHTML = html; })
|
|
34
|
+
.then(function (response) { if (!response.ok) throw response; return response.text(); })
|
|
35
|
+
.then(function (html) { out.innerHTML = html; })
|
|
36
|
+
.catch(function () { out.innerHTML = '<p class="hrl-errors" role="alert">Search failed. Check your connection and try again.</p>'; });
|
|
36
37
|
}, 180);
|
|
37
38
|
});
|
|
38
39
|
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
Array.from(data.keys()).forEach(function (key) { if (/\[proofs\]/.test(key)) data.delete(key); });
|
|
14
14
|
data.delete("_method");
|
|
15
15
|
fetch(form.dataset.hrlTaxPreview, { method: "POST", body: data, credentials: "same-origin", headers: { "Accept": "text/html" } })
|
|
16
|
-
.then(function (response) {
|
|
17
|
-
.then(function (html) {
|
|
16
|
+
.then(function (response) { if (!response.ok) throw response; return response.text(); })
|
|
17
|
+
.then(function (html) { panel.innerHTML = html; })
|
|
18
|
+
.catch(function () { panel.innerHTML = '<p class="hrl-errors" role="alert">Could not work out the tax just now. Check your connection; Save still works.</p>'; });
|
|
18
19
|
}
|
|
19
20
|
var rupees = new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR", maximumFractionDigits: 0 });
|
|
20
21
|
// The limit bar and "₹X of the ₹Y limit counts" follow the amount as typed.
|
|
@@ -28,8 +28,8 @@ module HrLite
|
|
|
28
28
|
user = HrLite.user_klass.find(params[:user_id])
|
|
29
29
|
asset.assign_to!(user, actor: hr_current_user)
|
|
30
30
|
redirect_to admin_assets_path, notice: "#{asset.name} given to #{HrLite.display_name(user)}."
|
|
31
|
-
rescue ActiveRecord::RecordInvalid
|
|
32
|
-
redirect_to admin_assets_path, alert: "#{asset.name} is already with somebody."
|
|
31
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
32
|
+
redirect_to admin_assets_path, alert: reason_for(e, "#{asset.name} is already with somebody.")
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def take_back
|
|
@@ -37,8 +37,8 @@ module HrLite
|
|
|
37
37
|
asset.return!(actor: hr_current_user, condition: params[:condition_note],
|
|
38
38
|
status: params[:status].presence_in(Asset::STATUSES) || "available")
|
|
39
39
|
redirect_to admin_assets_path, notice: "#{asset.name} taken back."
|
|
40
|
-
rescue ActiveRecord::RecordInvalid
|
|
41
|
-
redirect_to admin_assets_path, alert: "#{asset.name} is not with anybody."
|
|
40
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
41
|
+
redirect_to admin_assets_path, alert: reason_for(e, "#{asset.name} is not with anybody.")
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
private
|
|
@@ -87,7 +87,7 @@ module HrLite
|
|
|
87
87
|
notice: "Nothing to remove for that day."
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
record.destroy
|
|
90
|
+
record.destroy!
|
|
91
91
|
# Its late penalties go too, so checking in again is not charged twice.
|
|
92
92
|
AttendancePenalty.active.where(attendance_record_id: record.id)
|
|
93
93
|
.find_each { |p| p.update!(removed_at: Time.current, removed_by_id: hr_current_user.id, removal_reason: note) }
|
|
@@ -51,8 +51,8 @@ module HrLite
|
|
|
51
51
|
def resolve
|
|
52
52
|
CaReview.find(params[:id]).resolve!(actor: hr_current_user, note: params[:note])
|
|
53
53
|
redirect_back_to_list notice: "Marked resolved."
|
|
54
|
-
rescue ActiveRecord::RecordInvalid
|
|
55
|
-
redirect_back_to_list alert: "Only a flagged item can be resolved."
|
|
54
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
55
|
+
redirect_back_to_list alert: reason_for(e, "Only a flagged item can be resolved.")
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
private
|
|
@@ -18,8 +18,8 @@ module HrLite
|
|
|
18
18
|
notice: "Approved — #{request.credit_days.to_f} day credited."
|
|
19
19
|
rescue CompOffRequest::MissingCompOffType, CompOffRequest::StaleOffDay => e
|
|
20
20
|
redirect_to admin_comp_off_request_path(request), alert: e.message
|
|
21
|
-
rescue ActiveRecord::RecordInvalid
|
|
22
|
-
redirect_to admin_comp_off_request_path(request), alert: "Only pending requests can be decided."
|
|
21
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
22
|
+
redirect_to admin_comp_off_request_path(request), alert: reason_for(e, "Only pending requests can be decided.")
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def reject
|
|
@@ -31,8 +31,8 @@ module HrLite
|
|
|
31
31
|
|
|
32
32
|
request.reject!(actor: hr_current_user, note: note)
|
|
33
33
|
redirect_to admin_comp_off_requests_path, notice: "Request rejected."
|
|
34
|
-
rescue ActiveRecord::RecordInvalid
|
|
35
|
-
redirect_to admin_comp_off_request_path(request), alert: "Only pending requests can be decided."
|
|
34
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
35
|
+
redirect_to admin_comp_off_request_path(request), alert: reason_for(e, "Only pending requests can be decided.")
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
private
|
|
@@ -12,7 +12,10 @@ module HrLite
|
|
|
12
12
|
@change = DesignationChange.new(change_params.merge(
|
|
13
13
|
user_id: @profile.user_id, created_by_id: hr_current_user.id
|
|
14
14
|
))
|
|
15
|
-
if @change.save
|
|
15
|
+
if @change.save && @change.hook_error
|
|
16
|
+
redirect_to admin_employee_path(@profile),
|
|
17
|
+
alert: "Role change recorded, but it could not be applied to their login (#{@change.hook_error}) — update their access by hand."
|
|
18
|
+
elsif @change.persisted?
|
|
16
19
|
redirect_to admin_employee_path(@profile), notice: "Role change recorded."
|
|
17
20
|
else
|
|
18
21
|
render :new, status: :unprocessable_entity
|
|
@@ -101,13 +101,15 @@ module HrLite
|
|
|
101
101
|
# since an unreturned laptop and un-revoked access both outlive the
|
|
102
102
|
# employment.
|
|
103
103
|
ChecklistItem.open_for!(@profile.user, kind: "offboarding", anchor_date: exit_date)
|
|
104
|
+
done = "Offboarded — last day #{exit_date.strftime('%d %b %Y')}"
|
|
104
105
|
begin
|
|
105
106
|
HrLite.config.offboard_user.call(@profile.user)
|
|
106
107
|
rescue => e
|
|
107
108
|
Rails.logger.error("[hr_lite] offboard_user failed: #{e.class}: #{e.message}")
|
|
109
|
+
return redirect_to admin_employee_path(@profile),
|
|
110
|
+
alert: "#{done}, but access was NOT revoked (#{e.message.truncate(120)}) — revoke the login by hand."
|
|
108
111
|
end
|
|
109
|
-
redirect_to admin_employee_path(@profile),
|
|
110
|
-
notice: "Offboarded — last day #{exit_date.strftime('%d %b %Y')}, access revoked."
|
|
112
|
+
redirect_to admin_employee_path(@profile), notice: "#{done}, access revoked."
|
|
111
113
|
rescue ActiveRecord::RecordInvalid => e
|
|
112
114
|
redirect_to admin_employee_path(@profile), alert: e.record.errors.full_messages.to_sentence
|
|
113
115
|
end
|
|
@@ -15,8 +15,8 @@ module HrLite
|
|
|
15
15
|
expense = decidable.find(params[:id])
|
|
16
16
|
expense.approve!(actor: hr_current_user, note: params[:decision_note].presence)
|
|
17
17
|
redirect_to admin_expenses_path, notice: "Claim approved."
|
|
18
|
-
rescue ActiveRecord::RecordInvalid
|
|
19
|
-
redirect_to admin_expenses_path, alert: "Only a submitted claim can be approved."
|
|
18
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
19
|
+
redirect_to admin_expenses_path, alert: reason_for(e, "Only a submitted claim can be approved.")
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def reject
|
|
@@ -26,8 +26,8 @@ module HrLite
|
|
|
26
26
|
|
|
27
27
|
expense.reject!(actor: hr_current_user, note: note)
|
|
28
28
|
redirect_to admin_expenses_path, notice: "Claim rejected."
|
|
29
|
-
rescue ActiveRecord::RecordInvalid
|
|
30
|
-
redirect_to admin_expenses_path, alert: "Only a submitted claim can be rejected."
|
|
29
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
30
|
+
redirect_to admin_expenses_path, alert: reason_for(e, "Only a submitted claim can be rejected.")
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def reimburse
|
|
@@ -35,8 +35,8 @@ module HrLite
|
|
|
35
35
|
month = parse_month_param(params[:period_month])
|
|
36
36
|
expense.reimburse!(actor: hr_current_user, period_month: month)
|
|
37
37
|
redirect_to admin_expenses_path, notice: "Will be paid with #{month.strftime('%B %Y')} payroll."
|
|
38
|
-
rescue ActiveRecord::RecordInvalid
|
|
39
|
-
redirect_to admin_expenses_path, alert: "Only an approved claim can go on payroll, and only on a month not yet paid."
|
|
38
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
39
|
+
redirect_to admin_expenses_path, alert: reason_for(e, "Only an approved claim can go on payroll, and only on a month not yet paid.")
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# The receipt the employee had to upload, for the person deciding; same rules as a document download.
|
|
@@ -20,8 +20,8 @@ module HrLite
|
|
|
20
20
|
assignee = HrLite.user_klass.find(params[:assignee_id])
|
|
21
21
|
request.assign!(actor: hr_current_user, assignee: assignee)
|
|
22
22
|
redirect_to admin_hr_request_path(request), notice: "Assigned to #{HrLite.display_name(assignee)}."
|
|
23
|
-
rescue ActiveRecord::RecordInvalid
|
|
24
|
-
redirect_to admin_hr_request_path(request), alert: "This request is already settled."
|
|
23
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
24
|
+
redirect_to admin_hr_request_path(request), alert: reason_for(e, "This request is already settled.")
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def resolve
|
|
@@ -30,8 +30,8 @@ module HrLite
|
|
|
30
30
|
redirect_to admin_hr_requests_path, notice: "Answered."
|
|
31
31
|
rescue ArgumentError
|
|
32
32
|
redirect_to admin_hr_request_path(request), alert: "Write an answer before resolving."
|
|
33
|
-
rescue ActiveRecord::RecordInvalid
|
|
34
|
-
redirect_to admin_hr_request_path(request), alert: "This request is already settled."
|
|
33
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
34
|
+
redirect_to admin_hr_request_path(request), alert: reason_for(e, "This request is already settled.")
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
private
|
|
@@ -65,8 +65,8 @@ module HrLite
|
|
|
65
65
|
redirect_to admin_leave_request_path(request),
|
|
66
66
|
alert: "Cannot approve — comp-off credit no longer covers this request."
|
|
67
67
|
end
|
|
68
|
-
rescue ActiveRecord::RecordInvalid
|
|
69
|
-
redirect_to admin_leave_request_path(request), alert: "Only pending requests can be decided."
|
|
68
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
69
|
+
redirect_to admin_leave_request_path(request), alert: reason_for(e, "Only pending requests can be decided.")
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
# LeaveRequest#cancellable_by? has always allowed an admin to call off
|
|
@@ -96,8 +96,8 @@ module HrLite
|
|
|
96
96
|
|
|
97
97
|
request.reject!(actor: hr_current_user, note: note)
|
|
98
98
|
redirect_to admin_leave_requests_path, notice: "Leave rejected."
|
|
99
|
-
rescue ActiveRecord::RecordInvalid
|
|
100
|
-
redirect_to admin_leave_request_path(request), alert: "Only pending requests can be decided."
|
|
99
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
100
|
+
redirect_to admin_leave_request_path(request), alert: reason_for(e, "Only pending requests can be decided.")
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
private
|
|
@@ -6,6 +6,11 @@ module HrLite
|
|
|
6
6
|
def index
|
|
7
7
|
@query = OverviewQuery.new(user_ids: hr_access.visible_user_ids("leave.view"))
|
|
8
8
|
@kpis = @query.kpis
|
|
9
|
+
# Emails, bells and audit rows that failed in the background; the audit trail's own gate.
|
|
10
|
+
return unless hr_can?("profile.manage", scope: :all)
|
|
11
|
+
|
|
12
|
+
failures = AuditLog.recent.where("action LIKE ?", "failure.%").includes(:subject).limit(SECTION_CAP)
|
|
13
|
+
@failures = hr_superadmin? ? failures : failures.outside_money_tier
|
|
9
14
|
end
|
|
10
15
|
end
|
|
11
16
|
end
|
|
@@ -18,8 +18,8 @@ module HrLite
|
|
|
18
18
|
rescue RegularizationRequest::InvalidMerge => e
|
|
19
19
|
redirect_to admin_regularization_request_path(request),
|
|
20
20
|
alert: "Cannot apply — #{e.message}. Fix the day manually or reject with a note."
|
|
21
|
-
rescue ActiveRecord::RecordInvalid
|
|
22
|
-
redirect_to admin_regularization_request_path(request), alert: "Only pending tickets can be decided."
|
|
21
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
22
|
+
redirect_to admin_regularization_request_path(request), alert: reason_for(e, "Only pending tickets can be decided.")
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def reject
|
|
@@ -31,8 +31,8 @@ module HrLite
|
|
|
31
31
|
|
|
32
32
|
request.reject!(actor: hr_current_user, note: note)
|
|
33
33
|
redirect_to admin_regularization_requests_path, notice: "Ticket rejected."
|
|
34
|
-
rescue ActiveRecord::RecordInvalid
|
|
35
|
-
redirect_to admin_regularization_request_path(request), alert: "Only pending tickets can be decided."
|
|
34
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
35
|
+
redirect_to admin_regularization_request_path(request), alert: reason_for(e, "Only pending tickets can be decided.")
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
private
|
|
@@ -17,6 +17,10 @@ module HrLite
|
|
|
17
17
|
"#{hr_display_name(resignation.user)}, so no exit date was recorded — " \
|
|
18
18
|
"create the profile to clip attendance and payroll."
|
|
19
19
|
end
|
|
20
|
+
if resignation.access_error
|
|
21
|
+
return redirect_to admin_employees_path,
|
|
22
|
+
alert: "#{notice} But access was NOT revoked (#{resignation.access_error}) — revoke the login by hand."
|
|
23
|
+
end
|
|
20
24
|
redirect_to admin_employees_path, notice: notice
|
|
21
25
|
rescue ActiveRecord::RecordInvalid => e
|
|
22
26
|
# A rejected exit date used to be reported as "not pending", which was
|
|
@@ -62,8 +62,8 @@ module HrLite
|
|
|
62
62
|
declaration.verify!(actor: hr_current_user, note: "Proof accepted")
|
|
63
63
|
end
|
|
64
64
|
redirect_to admin_tax_declarations_path, notice: "Accepted and verified — payroll uses these amounts now."
|
|
65
|
-
rescue ActiveRecord::RecordInvalid
|
|
66
|
-
redirect_to admin_tax_declaration_path(declaration), alert: "Only a submitted declaration can be accepted."
|
|
65
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
66
|
+
redirect_to admin_tax_declaration_path(declaration), alert: reason_for(e, "Only a submitted declaration can be accepted.")
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
def update
|
|
@@ -49,8 +49,8 @@ module HrLite
|
|
|
49
49
|
request = decidable.find(params[:id])
|
|
50
50
|
request.approve!(actor: hr_current_user, note: params[:decision_note].to_s.strip.presence)
|
|
51
51
|
redirect_to admin_wfh_requests_path, notice: "WFH approved for #{hr_display_name(request.user)}."
|
|
52
|
-
rescue ActiveRecord::RecordInvalid
|
|
53
|
-
redirect_to admin_wfh_request_path(request), alert: "Only pending requests can be decided."
|
|
52
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
53
|
+
redirect_to admin_wfh_request_path(request), alert: reason_for(e, "Only pending requests can be decided.")
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def reject
|
|
@@ -60,8 +60,8 @@ module HrLite
|
|
|
60
60
|
|
|
61
61
|
request.reject!(actor: hr_current_user, note: note)
|
|
62
62
|
redirect_to admin_wfh_requests_path, notice: "WFH rejected."
|
|
63
|
-
rescue ActiveRecord::RecordInvalid
|
|
64
|
-
redirect_to admin_wfh_request_path(request), alert: "Only pending requests can be decided."
|
|
63
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
64
|
+
redirect_to admin_wfh_request_path(request), alert: reason_for(e, "Only pending requests can be decided.")
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
# HR changes an approved or rejected request: dates (past allowed) and/or decision, with a reason.
|
|
@@ -38,11 +38,11 @@ module HrLite
|
|
|
38
38
|
body: note, path: "/work_locations",
|
|
39
39
|
bell_to: [ location.user ], email_to: [ location.user ])
|
|
40
40
|
rescue => e
|
|
41
|
-
|
|
41
|
+
HrLite.record_failure("work_location.#{status}", e, subject: location.user)
|
|
42
42
|
end
|
|
43
43
|
redirect_to admin_work_locations_path, notice: "Location #{status}."
|
|
44
|
-
rescue ActiveRecord::RecordInvalid
|
|
45
|
-
redirect_to admin_work_locations_path, alert: "Only pending locations can be decided."
|
|
44
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
45
|
+
redirect_to admin_work_locations_path, alert: reason_for(e, "Only pending locations can be decided.")
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
end
|
|
@@ -100,6 +100,11 @@ module HrLite
|
|
|
100
100
|
end
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
+
# The record's own reason when it has one; `fallback` names the usual cause.
|
|
104
|
+
def reason_for(error, fallback)
|
|
105
|
+
error.record.errors.full_messages.to_sentence.presence || fallback
|
|
106
|
+
end
|
|
107
|
+
|
|
103
108
|
# Strict param parsing: anything that isn't the exact expected format
|
|
104
109
|
# falls back to today (never 500s on a mangled URL).
|
|
105
110
|
def parse_month_param(value)
|
|
@@ -27,8 +27,8 @@ module HrLite
|
|
|
27
27
|
request = CompOffRequest.where(user_id: hr_current_user.id).find(params[:id])
|
|
28
28
|
request.cancel!(actor: hr_current_user)
|
|
29
29
|
redirect_to comp_off_requests_path, notice: "Request cancelled."
|
|
30
|
-
rescue ActiveRecord::RecordInvalid
|
|
31
|
-
redirect_to comp_off_requests_path, alert: "Only pending requests can be cancelled."
|
|
30
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
31
|
+
redirect_to comp_off_requests_path, alert: reason_for(e, "Only pending requests can be cancelled.")
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
private
|
|
@@ -30,8 +30,8 @@ module HrLite
|
|
|
30
30
|
request = own.find(params[:id])
|
|
31
31
|
request.cancel!(actor: hr_current_user)
|
|
32
32
|
redirect_to hr_requests_path, notice: "Request withdrawn."
|
|
33
|
-
rescue ActiveRecord::RecordInvalid
|
|
34
|
-
redirect_to hr_requests_path, alert: "That request has already been answered."
|
|
33
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
34
|
+
redirect_to hr_requests_path, alert: reason_for(e, "That request has already been answered.")
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
private
|
|
@@ -23,8 +23,8 @@ module HrLite
|
|
|
23
23
|
request = RegularizationRequest.where(user_id: hr_current_user.id).find(params[:id])
|
|
24
24
|
request.cancel!(actor: hr_current_user)
|
|
25
25
|
redirect_to regularization_requests_path, notice: "Day fix cancelled."
|
|
26
|
-
rescue ActiveRecord::RecordInvalid
|
|
27
|
-
redirect_to regularization_requests_path, alert: "Only pending day fixes can be cancelled."
|
|
26
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
27
|
+
redirect_to regularization_requests_path, alert: reason_for(e, "Only pending day fixes can be cancelled.")
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
private
|
|
@@ -54,8 +54,8 @@ module HrLite
|
|
|
54
54
|
|
|
55
55
|
@declaration.submit!(actor: hr_current_user)
|
|
56
56
|
redirect_to tax_declaration_path, notice: "Submitted — HR will check the proof."
|
|
57
|
-
rescue ActiveRecord::RecordInvalid
|
|
58
|
-
redirect_to tax_declaration_path, alert: "Only a draft can be submitted."
|
|
57
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
58
|
+
redirect_to tax_declaration_path, alert: reason_for(e, "Only a draft can be submitted.")
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
private
|
|
@@ -26,8 +26,8 @@ module HrLite
|
|
|
26
26
|
def cancel
|
|
27
27
|
WfhRequest.where(user_id: hr_current_user.id).find(params[:id]).cancel!(actor: hr_current_user)
|
|
28
28
|
redirect_to wfh_requests_path, notice: "WFH request cancelled."
|
|
29
|
-
rescue ActiveRecord::RecordInvalid
|
|
30
|
-
redirect_to wfh_requests_path, alert: "Only pending requests can be cancelled."
|
|
29
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
30
|
+
redirect_to wfh_requests_path, alert: reason_for(e, "Only pending requests can be cancelled.")
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
private
|
|
@@ -50,7 +50,7 @@ module HrLite
|
|
|
50
50
|
def notify(event, title, path, to)
|
|
51
51
|
Notifications.publish(event, title: title, path: path, bell_to: to)
|
|
52
52
|
rescue => e
|
|
53
|
-
|
|
53
|
+
HrLite.record_failure(event, e, subject: hr_current_user)
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
end
|
|
@@ -33,7 +33,7 @@ module HrLite
|
|
|
33
33
|
.select { |m| m[:id].present? && m[:starts_at] && m[:ends_at] }
|
|
34
34
|
.each { |m| m[:id] = m[:id].to_s }
|
|
35
35
|
rescue => e
|
|
36
|
-
|
|
36
|
+
HrLite.record_failure("calendar_read", e, subject: user)
|
|
37
37
|
nil
|
|
38
38
|
end
|
|
39
39
|
|
|
@@ -37,8 +37,8 @@ module HrLite
|
|
|
37
37
|
hr_lite_publish_policy_change(log)
|
|
38
38
|
rescue => e
|
|
39
39
|
# The trail is best-effort by design: an audit hiccup must never roll
|
|
40
|
-
# back the domain write it describes.
|
|
41
|
-
|
|
40
|
+
# back the domain write it describes. Shown to HR admins instead.
|
|
41
|
+
HrLite.record_failure("audit.#{action}", e, subject: self)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def hr_lite_audited_changes(action)
|
|
@@ -17,6 +17,9 @@ module HrLite
|
|
|
17
17
|
after_create :apply_to_profile
|
|
18
18
|
after_create :notify
|
|
19
19
|
|
|
20
|
+
# Why the host's on_designation_change hook failed, for the screen to say so.
|
|
21
|
+
attr_reader :hook_error
|
|
22
|
+
|
|
20
23
|
def readonly?
|
|
21
24
|
persisted? && !destroyed?
|
|
22
25
|
end
|
|
@@ -45,6 +48,7 @@ module HrLite
|
|
|
45
48
|
HrLite.config.on_designation_change.call(user, to_designation)
|
|
46
49
|
rescue => e
|
|
47
50
|
Rails.logger.error("[hr_lite] on_designation_change failed: #{e.class}: #{e.message}")
|
|
51
|
+
@hook_error = e.message.truncate(120)
|
|
48
52
|
end
|
|
49
53
|
end
|
|
50
54
|
|
|
@@ -234,7 +234,7 @@ module HrLite
|
|
|
234
234
|
ChecklistItem.open_for!(user, kind: "onboarding", anchor_date: date_of_joining)
|
|
235
235
|
rescue => e
|
|
236
236
|
# A checklist must never be the reason an employee cannot be created.
|
|
237
|
-
|
|
237
|
+
HrLite.record_failure("onboarding_checklist", e, subject: user)
|
|
238
238
|
end
|
|
239
239
|
end
|
|
240
240
|
end
|
|
@@ -68,6 +68,9 @@ module HrLite
|
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
# False when the person had no employee profile to stamp — see accept!.
|
|
71
|
+
# Why the host could not revoke access, for the screen to say so.
|
|
72
|
+
attr_reader :access_error
|
|
73
|
+
|
|
71
74
|
def exit_date_recorded?
|
|
72
75
|
@exit_date_recorded
|
|
73
76
|
end
|
|
@@ -82,6 +85,7 @@ module HrLite
|
|
|
82
85
|
HrLite.config.offboard_user.call(user)
|
|
83
86
|
rescue => e
|
|
84
87
|
Rails.logger.error("[hr_lite] offboard_user failed: #{e.class}: #{e.message}")
|
|
88
|
+
@access_error = e.message.truncate(120)
|
|
85
89
|
end
|
|
86
90
|
|
|
87
91
|
def notify_submitted
|
|
@@ -96,7 +96,7 @@ module HrLite
|
|
|
96
96
|
Notifications.publish(event, title: title, body: body.presence, path: path, bell_to: to,
|
|
97
97
|
email_to: event == "wfh.requested" ? [] : to)
|
|
98
98
|
rescue => e
|
|
99
|
-
|
|
99
|
+
HrLite.record_failure(event, e, subject: user)
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
def dates_in_window
|
|
@@ -12,6 +12,23 @@
|
|
|
12
12
|
<div class="hrl-kpi"><div class="hrl-kpi__value"><%= @kpis[:missing_checkout] %></div><div class="hrl-kpi__label">Missing checkouts</div></div>
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
|
+
<% if @failures.present? %>
|
|
16
|
+
<section class="hrl-card">
|
|
17
|
+
<h2 class="hrl-card__title">Failed in the background</h2>
|
|
18
|
+
<% @failures.each do |log| %>
|
|
19
|
+
<div class="hrl-feed__item">
|
|
20
|
+
<div class="hrl-feed__body">
|
|
21
|
+
<strong><%= log.action.delete_prefix("failure.").tr("._", " ").humanize %></strong>
|
|
22
|
+
— <%= log.subject.is_a?(HrLite.user_klass) ? hr_display_name(log.subject) : "#{log.subject_type.demodulize.underscore.humanize} ##{log.subject_id}" %>,
|
|
23
|
+
<%= log.created_at.strftime("%d %b, %H:%M") %>
|
|
24
|
+
<div class="hrl-small hrl-muted"><%= log.audited_changes["error"] %>: <%= log.audited_changes["message"] %></div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<% end %>
|
|
28
|
+
<a class="hrl-card__link" href="<%= hr_lite.admin_audit_logs_path %>">Audit trail</a>
|
|
29
|
+
</section>
|
|
30
|
+
<% end %>
|
|
31
|
+
|
|
15
32
|
<% if pending.zero? && @kpis.except(:pending).values.all?(&:zero?) %>
|
|
16
33
|
<p class="hrl-card hrl-card--empty">All present and accounted for.</p>
|
|
17
34
|
<% else %>
|
|
@@ -110,7 +110,7 @@ module HrLite
|
|
|
110
110
|
lead_path = leadership[:path] || path
|
|
111
111
|
|
|
112
112
|
deliver_bells(event, row, bell_to, title, body, path)
|
|
113
|
-
deliver_emails(row, email_to, title, body, path, lines, link_url)
|
|
113
|
+
deliver_emails(event, row, email_to, title, body, path, lines, link_url)
|
|
114
114
|
return if skip_leadership
|
|
115
115
|
|
|
116
116
|
deliver_leadership_email(event, row, lead_title, lead_body, lead_path, lines, diff)
|
|
@@ -128,7 +128,7 @@ module HrLite
|
|
|
128
128
|
end
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
-
def deliver_emails(row, email_to, title, body, path, lines, link_url = nil)
|
|
131
|
+
def deliver_emails(event, row, email_to, title, body, path, lines, link_url = nil)
|
|
132
132
|
return unless row[:email]
|
|
133
133
|
|
|
134
134
|
# Rescued PER RECIPIENT. Wrapping the whole loop meant one bad address
|
|
@@ -147,7 +147,7 @@ module HrLite
|
|
|
147
147
|
held = held_until[user.id]
|
|
148
148
|
held ? mail.deliver_later(wait_until: held) : mail.deliver_later
|
|
149
149
|
rescue => e
|
|
150
|
-
|
|
150
|
+
HrLite.record_failure("#{event}.email", e, subject: user)
|
|
151
151
|
end
|
|
152
152
|
end
|
|
153
153
|
end
|
|
@@ -162,7 +162,7 @@ module HrLite
|
|
|
162
162
|
body: body, lines: lines, diff: diff, path: path, event: event.to_s)
|
|
163
163
|
.deliver_later
|
|
164
164
|
rescue => e
|
|
165
|
-
|
|
165
|
+
HrLite.record_failure("#{event}.leadership_email", e)
|
|
166
166
|
end
|
|
167
167
|
|
|
168
168
|
def deliver_leadership_bells(event, row, bell_to, title, body, path)
|
|
@@ -175,7 +175,7 @@ module HrLite
|
|
|
175
175
|
HrLite.notify(user: leader, kind: event.to_s, title: title, body: body, path: path)
|
|
176
176
|
end
|
|
177
177
|
rescue => e
|
|
178
|
-
|
|
178
|
+
HrLite.record_failure("#{event}.leadership_bell", e)
|
|
179
179
|
end
|
|
180
180
|
end
|
|
181
181
|
end
|
data/lib/hr_lite/version.rb
CHANGED
data/lib/hr_lite.rb
CHANGED
|
@@ -173,8 +173,18 @@ module HrLite
|
|
|
173
173
|
def notify(user:, kind:, title:, body: nil, path: nil)
|
|
174
174
|
config.notify.call(user: user, kind: kind, title: title, body: body, path: path)
|
|
175
175
|
rescue => e
|
|
176
|
-
|
|
176
|
+
record_failure("notify", e, subject: user)
|
|
177
177
|
nil
|
|
178
178
|
end
|
|
179
|
+
|
|
180
|
+
# A background side effect failed: log it and keep a row that HR admins
|
|
181
|
+
# see on the overview, so nothing fails where nobody looks. Never raises.
|
|
182
|
+
def record_failure(event, error, subject: nil)
|
|
183
|
+
Rails.logger.error("[hr_lite] #{event} failed: #{error.class}: #{error.message}")
|
|
184
|
+
AuditLog.create!(actor: Current.actor, action: "failure.#{event}", subject_type: subject ? subject.class.name : config.user_class,
|
|
185
|
+
subject_id: subject&.id || 0, audited_changes: { "error" => error.class.name, "message" => error.message.to_s.truncate(200) })
|
|
186
|
+
rescue => e
|
|
187
|
+
Rails.logger.error("[hr_lite] recording a failure failed: #{e.class}: #{e.message}")
|
|
188
|
+
end
|
|
179
189
|
end
|
|
180
190
|
end
|