hr_lite 0.57.1 → 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 +23 -0
- data/README.md +1 -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/home_controller.rb +3 -1
- 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/jobs/hr_lite/team_notices_job.rb +26 -0
- 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 +14 -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/app/views/hr_lite/home/index.html.erb +16 -0
- data/app/views/hr_lite/leave_requests/_out_list.html.erb +1 -1
- data/lib/hr_lite/notifications.rb +8 -5
- data/lib/hr_lite/version.rb +1 -1
- data/lib/hr_lite.rb +11 -1
- metadata +2 -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,29 @@ 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
|
+
|
|
24
|
+
## [0.58.0] - 2026-10-21
|
|
25
|
+
|
|
26
|
+
Leave and birthdays for the whole team, Keka-style. No migrations. Hosts schedule `HrLite::TeamNoticesJob` daily at 10am.
|
|
27
|
+
|
|
28
|
+
- **Pending leave shows on Home.** "On leave this week" lists pending requests too, tagged "Pending approval". Approval is management's decision, not a reason to hide the leave.
|
|
29
|
+
- **10am "On leave today" email** to every current employee: name and dates, never the leave type. No one out, no email.
|
|
30
|
+
- **10am "Birthdays today" email**, same rules. A 29 Feb birthday falls on 28 Feb outside leap years.
|
|
31
|
+
- **Birthdays this week** card on Home, Monday to Sunday, today marked.
|
|
32
|
+
|
|
10
33
|
## [0.57.1] - 2026-10-19
|
|
11
34
|
|
|
12
35
|
Forms in the host's iOS and Android staff apps (D24). No migrations.
|
data/README.md
CHANGED
|
@@ -384,6 +384,7 @@ whatever you run). Each is idempotent and sends nothing on a quiet day.
|
|
|
384
384
|
| Job | Schedule | Purpose |
|
|
385
385
|
|---|---|---|
|
|
386
386
|
| `HrLite::DailyDigestJob` | each morning | Leadership digest: who is out today, pending approvals, flagged punches, missing checkouts |
|
|
387
|
+
| `HrLite::TeamNoticesJob` | daily, 10am | Emails every current employee who is on leave today and whose birthday it is; each email is skipped when its list is empty |
|
|
387
388
|
| `HrLite::PayrollAutoDraftJob` | monthly, on the 1st | Draft and compute the previous month's payroll from attendance, then notify the approvers (`payroll.approve`) and payroll operators |
|
|
388
389
|
| `HrLite::CaReviewReminderJob` | weekly (it acts only in March) | Tell CA reviewers how many slips and declarations are still unverified before 31 March |
|
|
389
390
|
| `HrLite::CaReviewOverdueJob` | 31 March, evening | Tell payroll and HR what the CA has not verified, and any flags left open |
|
|
@@ -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
|
|
@@ -3,9 +3,11 @@ module HrLite
|
|
|
3
3
|
def index
|
|
4
4
|
@latest_kudos = Kudo.recent.includes(:giver, kudo_mentions: :user).limit(3)
|
|
5
5
|
@awards = Award.current
|
|
6
|
-
|
|
6
|
+
# Pending shows too: approval is management's call, not a reason to hide the leave.
|
|
7
|
+
@out_week = LeaveRequest.where(status: %w[pending approved]).overlapping_range(Date.current, Date.current.end_of_week)
|
|
7
8
|
.includes(:user).where(user_id: HrLite.active_employees.map(&:id))
|
|
8
9
|
.sort_by { |leave| hr_display_name(leave.user).downcase }
|
|
10
|
+
@birthdays = EmployeeProfile.birthdays_in(Date.current.all_week)
|
|
9
11
|
@attention = attention_days
|
|
10
12
|
@waiting = Approval.pending_for(hr_current_user).count
|
|
11
13
|
@mine_pending = [ LeaveRequest.includes(:leave_type), RegularizationRequest, CompOffRequest, WfhRequest ]
|
|
@@ -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
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# 10am email to every current employee: who is on leave today, whose
|
|
3
|
+
# birthday it is. Each goes only when its list has someone on it.
|
|
4
|
+
class TeamNoticesJob < ApplicationJob
|
|
5
|
+
queue_as :default
|
|
6
|
+
|
|
7
|
+
def perform(date: Date.current)
|
|
8
|
+
staff = HrLite.active_employees(on: date)
|
|
9
|
+
out = LeaveRequest.where(status: %w[pending approved], user_id: staff.map(&:id))
|
|
10
|
+
.overlapping_range(date, date).includes(:user)
|
|
11
|
+
announce("leave.out_today", "On leave today", staff, date,
|
|
12
|
+
out.map { |l| "#{HrLite.display_name(l.user)} — #{l.date_range_label}#{' (pending approval)' if l.pending?}" })
|
|
13
|
+
announce("birthday.today", "Birthdays today", staff, date,
|
|
14
|
+
EmployeeProfile.birthdays_in(date..date).map { |p, _| "Happy birthday, #{HrLite.display_name(p.user)}" })
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def announce(event, title, staff, date, lines)
|
|
20
|
+
return if lines.empty?
|
|
21
|
+
|
|
22
|
+
Notifications.publish(event, title: "#{title} — #{date.strftime('%a %-d %b')}", lines: lines.sort,
|
|
23
|
+
email_to: staff, path: "/")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -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
|
|
|
@@ -101,6 +101,19 @@ module HrLite
|
|
|
101
101
|
date_of_joining <= date && (date_of_exit.nil? || date_of_exit >= date)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
# 29 Feb birthdays fall on 28 Feb outside leap years.
|
|
105
|
+
def birthday_in(year)
|
|
106
|
+
day = Date.valid_date?(year, date_of_birth.month, date_of_birth.day) ? date_of_birth.day : 28
|
|
107
|
+
Date.new(year, date_of_birth.month, day)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# [profile, day] for current staff whose birthday falls in `range`, earliest first.
|
|
111
|
+
def self.birthdays_in(range)
|
|
112
|
+
where(user_id: HrLite.active_employees(on: range.first).map(&:id)).where.not(date_of_birth: nil).includes(:user)
|
|
113
|
+
.filter_map { |p| (day = range.find { |d| p.birthday_in(d.year) == d }) && [ p, day ] }
|
|
114
|
+
.sort_by { |p, day| [ day, HrLite.display_name(p.user).downcase ] }
|
|
115
|
+
end
|
|
116
|
+
|
|
104
117
|
def employment_range_in(month)
|
|
105
118
|
from = [ date_of_joining, month.beginning_of_month ].max
|
|
106
119
|
to = [ date_of_exit || month.end_of_month, month.end_of_month ].min
|
|
@@ -221,7 +234,7 @@ module HrLite
|
|
|
221
234
|
ChecklistItem.open_for!(user, kind: "onboarding", anchor_date: date_of_joining)
|
|
222
235
|
rescue => e
|
|
223
236
|
# A checklist must never be the reason an employee cannot be created.
|
|
224
|
-
|
|
237
|
+
HrLite.record_failure("onboarding_checklist", e, subject: user)
|
|
225
238
|
end
|
|
226
239
|
end
|
|
227
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 %>
|
|
@@ -40,6 +40,22 @@
|
|
|
40
40
|
<%= render "hr_lite/leave_requests/out_list", leaves: @out_week, empty: "Nobody is on leave this week." %>
|
|
41
41
|
</section>
|
|
42
42
|
|
|
43
|
+
<section class="hrl-card">
|
|
44
|
+
<h2 class="hrl-card__title">Birthdays this week</h2>
|
|
45
|
+
<% if @birthdays.any? %>
|
|
46
|
+
<div class="hrl-feed">
|
|
47
|
+
<% @birthdays.each do |profile, day| %>
|
|
48
|
+
<div class="hrl-feed__item">
|
|
49
|
+
<div class="hrl-feed__body"><strong><%= hr_display_name(profile.user) %></strong>
|
|
50
|
+
<div class="hrl-feed__meta"><%= day == Date.current ? "Today" : day.strftime("%a %-d %b") %></div></div>
|
|
51
|
+
</div>
|
|
52
|
+
<% end %>
|
|
53
|
+
</div>
|
|
54
|
+
<% else %>
|
|
55
|
+
<p class="hrl-muted hrl-small">No birthdays this week.</p>
|
|
56
|
+
<% end %>
|
|
57
|
+
</section>
|
|
58
|
+
|
|
43
59
|
<% if @awards.any? %>
|
|
44
60
|
<section class="hrl-card">
|
|
45
61
|
<h2 class="hrl-card__title">Employee of the month, quarter and year</h2>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<% leaves.first(10).each do |leave| %>
|
|
5
5
|
<div class="hrl-feed__item">
|
|
6
6
|
<div class="hrl-feed__body"><%= tag.strong hr_display_name(leave.user), title: "On leave: #{leave.date_range_label}" %>
|
|
7
|
-
<div class="hrl-feed__meta"><%= leave.date_range_label %></div></div>
|
|
7
|
+
<div class="hrl-feed__meta"><%= leave.date_range_label %><%= " · Pending approval" if leave.pending? %></div></div>
|
|
8
8
|
</div>
|
|
9
9
|
<% end %>
|
|
10
10
|
</div>
|
|
@@ -56,6 +56,9 @@ module HrLite
|
|
|
56
56
|
"policy.published" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
57
57
|
"holiday.published" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
58
58
|
"digest.daily" => { bell: false, email: false, leadership_email: true, leadership_bell: false },
|
|
59
|
+
# 10am team emails (TeamNoticesJob) to every current employee; skipped when the list is empty.
|
|
60
|
+
"leave.out_today" => { bell: false, email: true, leadership_email: false, leadership_bell: false },
|
|
61
|
+
"birthday.today" => { bell: false, email: true, leadership_email: false, leadership_bell: false },
|
|
59
62
|
"resignation.submitted" => { bell: true, email: false, leadership_email: true, leadership_bell: true },
|
|
60
63
|
"resignation.accepted" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
|
|
61
64
|
"resignation.withdrawn" => { bell: true, email: false, leadership_email: true, leadership_bell: false },
|
|
@@ -107,7 +110,7 @@ module HrLite
|
|
|
107
110
|
lead_path = leadership[:path] || path
|
|
108
111
|
|
|
109
112
|
deliver_bells(event, row, bell_to, title, body, path)
|
|
110
|
-
deliver_emails(row, email_to, title, body, path, lines, link_url)
|
|
113
|
+
deliver_emails(event, row, email_to, title, body, path, lines, link_url)
|
|
111
114
|
return if skip_leadership
|
|
112
115
|
|
|
113
116
|
deliver_leadership_email(event, row, lead_title, lead_body, lead_path, lines, diff)
|
|
@@ -125,7 +128,7 @@ module HrLite
|
|
|
125
128
|
end
|
|
126
129
|
end
|
|
127
130
|
|
|
128
|
-
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)
|
|
129
132
|
return unless row[:email]
|
|
130
133
|
|
|
131
134
|
# Rescued PER RECIPIENT. Wrapping the whole loop meant one bad address
|
|
@@ -144,7 +147,7 @@ module HrLite
|
|
|
144
147
|
held = held_until[user.id]
|
|
145
148
|
held ? mail.deliver_later(wait_until: held) : mail.deliver_later
|
|
146
149
|
rescue => e
|
|
147
|
-
|
|
150
|
+
HrLite.record_failure("#{event}.email", e, subject: user)
|
|
148
151
|
end
|
|
149
152
|
end
|
|
150
153
|
end
|
|
@@ -159,7 +162,7 @@ module HrLite
|
|
|
159
162
|
body: body, lines: lines, diff: diff, path: path, event: event.to_s)
|
|
160
163
|
.deliver_later
|
|
161
164
|
rescue => e
|
|
162
|
-
|
|
165
|
+
HrLite.record_failure("#{event}.leadership_email", e)
|
|
163
166
|
end
|
|
164
167
|
|
|
165
168
|
def deliver_leadership_bells(event, row, bell_to, title, body, path)
|
|
@@ -172,7 +175,7 @@ module HrLite
|
|
|
172
175
|
HrLite.notify(user: leader, kind: event.to_s, title: title, body: body, path: path)
|
|
173
176
|
end
|
|
174
177
|
rescue => e
|
|
175
|
-
|
|
178
|
+
HrLite.record_failure("#{event}.leadership_bell", e)
|
|
176
179
|
end
|
|
177
180
|
end
|
|
178
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
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hr_lite
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.60.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kshitiz sinha
|
|
@@ -173,6 +173,7 @@ files:
|
|
|
173
173
|
- app/jobs/hr_lite/payroll_auto_draft_job.rb
|
|
174
174
|
- app/jobs/hr_lite/payroll_compute_job.rb
|
|
175
175
|
- app/jobs/hr_lite/slips_ready_job.rb
|
|
176
|
+
- app/jobs/hr_lite/team_notices_job.rb
|
|
176
177
|
- app/jobs/hr_lite/work_mode_expire_job.rb
|
|
177
178
|
- app/mailers/hr_lite/application_mailer.rb
|
|
178
179
|
- app/mailers/hr_lite/event_mailer.rb
|