hr_lite 0.54.0 → 0.55.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 +60 -0
- data/app/assets/javascripts/hr_lite/confirm.js +9 -5
- data/app/assets/javascripts/hr_lite/countdown.js +9 -2
- data/app/assets/javascripts/hr_lite/search.js +1 -1
- data/app/assets/stylesheets/hr_lite/hr_lite.css +18 -9
- data/app/controllers/hr_lite/admin/attendances_controller.rb +10 -3
- data/app/controllers/hr_lite/admin/audit_logs_controller.rb +1 -0
- data/app/controllers/hr_lite/admin/employees_controller.rb +11 -4
- data/app/controllers/hr_lite/admin/expenses_controller.rb +4 -0
- data/app/controllers/hr_lite/admin/hr_requests_controller.rb +4 -0
- data/app/controllers/hr_lite/admin/leave_requests_controller.rb +4 -1
- data/app/controllers/hr_lite/admin/payroll_runs_controller.rb +9 -5
- data/app/controllers/hr_lite/admin/reports_controller.rb +3 -2
- data/app/controllers/hr_lite/admin/salary_structures_controller.rb +2 -0
- data/app/controllers/hr_lite/admin/tax_declarations_controller.rb +13 -3
- data/app/controllers/hr_lite/approvals_controller.rb +1 -0
- data/app/controllers/hr_lite/home_controller.rb +2 -2
- data/app/controllers/hr_lite/regularization_requests_controller.rb +4 -4
- data/app/controllers/hr_lite/tax_declarations_controller.rb +9 -5
- data/app/helpers/hr_lite/application_helper.rb +14 -8
- data/app/jobs/hr_lite/attendance_close_job.rb +1 -1
- data/app/jobs/hr_lite/payroll_compute_job.rb +12 -0
- data/app/models/concerns/hr_lite/audited.rb +3 -1
- data/app/models/concerns/hr_lite/encrypted_money.rb +8 -5
- data/app/models/hr_lite/attendance_record.rb +14 -0
- data/app/models/hr_lite/comp_off_request.rb +2 -0
- data/app/models/hr_lite/document.rb +1 -1
- data/app/models/hr_lite/expense.rb +3 -0
- data/app/models/hr_lite/hr_request.rb +2 -0
- data/app/models/hr_lite/kudo.rb +2 -2
- data/app/models/hr_lite/leave_request.rb +25 -6
- data/app/models/hr_lite/payroll_line_item.rb +8 -0
- data/app/models/hr_lite/payroll_run.rb +15 -6
- data/app/models/hr_lite/regularization_request.rb +2 -2
- data/app/models/hr_lite/resignation.rb +5 -1
- data/app/models/hr_lite/role.rb +5 -0
- data/app/models/hr_lite/salary_slip.rb +1 -1
- data/app/models/hr_lite/tax_declaration.rb +5 -0
- data/app/services/hr_lite/attendance_puncher.rb +4 -0
- data/app/services/hr_lite/overview_query.rb +0 -12
- data/app/services/hr_lite/work_status.rb +5 -2
- data/app/views/hr_lite/admin/assets/index.html.erb +9 -2
- data/app/views/hr_lite/admin/attendances/show.html.erb +3 -1
- data/app/views/hr_lite/admin/attendances/summary.html.erb +5 -2
- data/app/views/hr_lite/admin/audit_logs/index.html.erb +6 -4
- data/app/views/hr_lite/admin/ca_reviews/index.html.erb +3 -3
- data/app/views/hr_lite/admin/checklists/index.html.erb +3 -3
- data/app/views/hr_lite/admin/comp_off_requests/index.html.erb +1 -2
- data/app/views/hr_lite/admin/comp_off_requests/show.html.erb +5 -5
- data/app/views/hr_lite/admin/designation_changes/new.html.erb +4 -3
- data/app/views/hr_lite/admin/documents/index.html.erb +8 -3
- data/app/views/hr_lite/admin/documents/show.html.erb +1 -0
- data/app/views/hr_lite/admin/employees/_form.html.erb +12 -5
- data/app/views/hr_lite/admin/employees/show.html.erb +38 -32
- data/app/views/hr_lite/admin/expenses/index.html.erb +3 -2
- data/app/views/hr_lite/admin/holidays/index.html.erb +5 -4
- data/app/views/hr_lite/admin/hr_requests/index.html.erb +1 -1
- data/app/views/hr_lite/admin/hr_requests/show.html.erb +4 -3
- data/app/views/hr_lite/admin/leave_balances/index.html.erb +6 -4
- data/app/views/hr_lite/admin/leave_requests/index.html.erb +1 -2
- data/app/views/hr_lite/admin/leave_requests/show.html.erb +5 -3
- data/app/views/hr_lite/admin/leave_types/edit.html.erb +4 -4
- data/app/views/hr_lite/admin/leave_types/index.html.erb +3 -3
- data/app/views/hr_lite/admin/loans/index.html.erb +1 -1
- data/app/views/hr_lite/admin/loans/show.html.erb +8 -12
- data/app/views/hr_lite/admin/office_locations/edit.html.erb +4 -4
- data/app/views/hr_lite/admin/payroll_runs/index.html.erb +2 -2
- data/app/views/hr_lite/admin/payroll_runs/show.html.erb +10 -7
- data/app/views/hr_lite/admin/policies/index.html.erb +1 -1
- data/app/views/hr_lite/admin/policies/new.html.erb +2 -2
- data/app/views/hr_lite/admin/regularization_requests/index.html.erb +3 -4
- data/app/views/hr_lite/admin/regularization_requests/show.html.erb +4 -5
- data/app/views/hr_lite/admin/reports/index.html.erb +3 -6
- data/app/views/hr_lite/admin/reports/show.html.erb +16 -2
- data/app/views/hr_lite/admin/roles/_form.html.erb +6 -5
- data/app/views/hr_lite/admin/salary_slips/show.html.erb +3 -2
- data/app/views/hr_lite/admin/salary_structures/_form.html.erb +1 -0
- data/app/views/hr_lite/admin/settings/edit.html.erb +5 -1
- data/app/views/hr_lite/admin/statutory_rate_cards/_form.html.erb +3 -3
- data/app/views/hr_lite/admin/statutory_rate_cards/index.html.erb +3 -1
- data/app/views/hr_lite/admin/tax_declarations/index.html.erb +1 -1
- data/app/views/hr_lite/admin/tax_declarations/show.html.erb +2 -0
- data/app/views/hr_lite/admin/wfh_requests/index.html.erb +17 -16
- data/app/views/hr_lite/admin/wfh_requests/show.html.erb +2 -4
- data/app/views/hr_lite/admin/work_locations/index.html.erb +1 -1
- data/app/views/hr_lite/admin/work_modes/index.html.erb +1 -1
- data/app/views/hr_lite/approvals/index.html.erb +1 -1
- data/app/views/hr_lite/attendance/_month_grid.html.erb +9 -9
- data/app/views/hr_lite/attendance/_penalties.html.erb +2 -2
- data/app/views/hr_lite/attendance/_punch_card.html.erb +2 -4
- data/app/views/hr_lite/attendance/_slot.html.erb +9 -6
- data/app/views/hr_lite/attendance/_trail.html.erb +1 -1
- data/app/views/hr_lite/attendance/_work_mode.html.erb +1 -1
- data/app/views/hr_lite/attendance/show.html.erb +4 -4
- data/app/views/hr_lite/calendar/show.html.erb +2 -2
- data/app/views/hr_lite/comp_off_requests/index.html.erb +1 -1
- data/app/views/hr_lite/comp_off_requests/new.html.erb +1 -1
- data/app/views/hr_lite/documents/_upload_form.html.erb +1 -1
- data/app/views/hr_lite/employee_profiles/show.html.erb +4 -2
- data/app/views/hr_lite/home/index.html.erb +2 -2
- data/app/views/hr_lite/id_cards/show.html.erb +5 -1
- data/app/views/hr_lite/leave_requests/_balance_chips.html.erb +2 -2
- data/app/views/hr_lite/leave_requests/new.html.erb +17 -8
- data/app/views/hr_lite/leave_requests/show.html.erb +2 -0
- data/app/views/hr_lite/org/show.html.erb +1 -1
- data/app/views/hr_lite/regularization_requests/index.html.erb +7 -7
- data/app/views/hr_lite/regularization_requests/new.html.erb +7 -5
- data/app/views/hr_lite/salary_slips/_slip_detail.html.erb +2 -2
- data/app/views/hr_lite/salary_slips/pdf.html.erb +1 -1
- data/app/views/hr_lite/tax/_computation.html.erb +1 -1
- data/app/views/hr_lite/tax_declarations/show.html.erb +4 -4
- data/app/views/hr_lite/team/show.html.erb +8 -7
- data/app/views/hr_lite/timelines/show.html.erb +1 -0
- data/app/views/hr_lite/wfh_requests/index.html.erb +3 -3
- data/app/views/hr_lite/work_time/_bars.html.erb +1 -1
- data/app/views/hr_lite/work_time/_donut.html.erb +1 -1
- data/app/views/hr_lite/work_time/_report.html.erb +14 -8
- data/app/views/layouts/hr_lite/application.html.erb +1 -0
- data/lib/hr_lite/notifications.rb +5 -2
- data/lib/hr_lite/version.rb +1 -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: 901c6516adf77c589f4e570c477d7aa2a7b05f279e5ef9d205cf868796074939
|
|
4
|
+
data.tar.gz: d3a6e968a40f6e5fe9d8503a174f8ce004c18ee6cf25218372e21a78d5fecc76
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 43c25d542b2eacd2af96e5ff8b281f608fb202be4c1c8ecdd4790b440843fb79ceb32596e8d641bb72beffc8c606dd558d65cd0bba5fbc4a860a8a59e5f24790
|
|
7
|
+
data.tar.gz: 2e1af9569c33bce9ca0ea5ef790b855d397e6ebf6c88a656757001849f0d197ee8901c057d19de835d999d80005dab3fd6e505ef26829167d3be7f9a101c834c
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.55.0] - 2026-10-17
|
|
11
|
+
|
|
12
|
+
The second pass of the HRMS audit of 0.54.0 covers bugs, security, performance and the remaining screen fixes (#115–#131). No migrations.
|
|
13
|
+
|
|
14
|
+
- **Payroll compute runs in a background job** (`HrLite::PayrollComputeJob`, default queue).
|
|
15
|
+
- The run page shows "Computing…" and refreshes until the run reaches review.
|
|
16
|
+
- A failed compute leaves the run as it was and adds a run warning, not an error page.
|
|
17
|
+
- Hosts need a running ActiveJob backend.
|
|
18
|
+
- **Money**
|
|
19
|
+
- A run computed before a leave decision, a day fix or a one-off item change now needs a recompute before it can be approved or published.
|
|
20
|
+
- "Record and verify" refuses blank verified amounts.
|
|
21
|
+
- Once an employee submits a tax declaration, they can no longer edit it.
|
|
22
|
+
- HR's verified amounts need a submitted declaration, and each change writes an audit row.
|
|
23
|
+
- Claims and help-desk tickets refuse decisions in the wrong state.
|
|
24
|
+
- "1,200" or "1,50,000" in a money field saves instead of failing.
|
|
25
|
+
- An employee can open only the current or the previous financial year's declaration.
|
|
26
|
+
- The tax panel labels its gross "before unpaid days".
|
|
27
|
+
- **Leave and attendance**
|
|
28
|
+
- Deciding a leave or a claim outside its approval flow settles the flow's other rows.
|
|
29
|
+
- The late ladder skips weekly offs, holidays and half-day leave.
|
|
30
|
+
- HR attendance fixes refuse future dates, times that fall on another day, and approved full-day leave.
|
|
31
|
+
- Optional holidays are no longer shaded as days off.
|
|
32
|
+
- A request warns when it and the other pending ones go past the balance.
|
|
33
|
+
- First-half and second-half leave on the same date no longer clash.
|
|
34
|
+
- The approver sees the balance as of the leave date.
|
|
35
|
+
- Inbox › All lists a flow leave once.
|
|
36
|
+
- The company-wide leave notice no longer names the leave type.
|
|
37
|
+
- Submit buttons lock after the first press.
|
|
38
|
+
- **Security and audit**
|
|
39
|
+
- HR cannot edit their own profile or salary, or offboard themselves. The pages return 404 and the buttons are hidden.
|
|
40
|
+
- Tax-proof links expire after 5 minutes and ID-card photo links after an hour.
|
|
41
|
+
- These now write audit rows: role permission changes, comp-off approvals, resignations, and payroll run create and delete.
|
|
42
|
+
- Audit rows redact date of birth and help-desk text.
|
|
43
|
+
- "Aadhaar" and "PAN" uploads reach the money tier whatever their case.
|
|
44
|
+
- Kudos mentions reach employees only.
|
|
45
|
+
- Offboarding removes the person's HR role assignments.
|
|
46
|
+
- **Inbox**
|
|
47
|
+
- Your own requests on a person's week open your own pages.
|
|
48
|
+
- Claim rows name the type, amount and date.
|
|
49
|
+
- Pending resignations are an Inbox queue and count in "N requests waiting".
|
|
50
|
+
- "One-off items" shows only to those who can run payroll.
|
|
51
|
+
- **Screens**
|
|
52
|
+
- Leave forms say what is wrong, and decision pages ask before acting.
|
|
53
|
+
- Payslips, tax working, tax states and loan facts read in words and whole days.
|
|
54
|
+
- Admin settings, roles (permission groups fold), policies, holidays and the audit trail fit a phone. Page views are hidden from the audit trail by default.
|
|
55
|
+
- "Fix a day" is the one name for attendance fixes.
|
|
56
|
+
- Compute leads a draft run, and the rate card in force is marked.
|
|
57
|
+
- Home shows this week's leave from today, and reports have month arrows.
|
|
58
|
+
- Person pages link back, and the employee form is grouped.
|
|
59
|
+
- People › "My team" is "Team board".
|
|
60
|
+
- Inbox › WFH puts Give WFH before a folded filter.
|
|
61
|
+
- Search has a visible Close.
|
|
62
|
+
- Button borders meet 3:1 contrast, and the sides clear the notch.
|
|
63
|
+
- List links are 44 px buttons, the help-desk pick starts blank, and work-time bars sit further apart.
|
|
64
|
+
- **Performance**
|
|
65
|
+
- Leave-balance grids, the report and the chips count each balance once.
|
|
66
|
+
- A leave notice reads every recipient's do-not-disturb in one query.
|
|
67
|
+
- A person's month, the work-time report and the payroll register each load in one query instead of one per day or slip.
|
|
68
|
+
- The overview drops an unused count.
|
|
69
|
+
|
|
10
70
|
## [0.54.0] - 2026-10-17
|
|
11
71
|
|
|
12
72
|
Fixes from the HRMS UI/UX audit of 0.52.0. No migrations.
|
|
@@ -26,11 +26,15 @@
|
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// Double-submit guard (data-disable-with is inert without Turbo/ujs)
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
setTimeout(function () {
|
|
29
|
+
// Double-submit guard (data-disable-with is inert without Turbo/ujs): every
|
|
30
|
+
// submit button of the form, Enter included. The timeout lets the
|
|
31
|
+
// submitter's value post first; geo punch runs its own.
|
|
32
|
+
if (!form.hasAttribute("data-hrl-geo-punch")) {
|
|
33
|
+
setTimeout(function () {
|
|
34
|
+
Array.prototype.forEach.call(form.elements, function (b) {
|
|
35
|
+
if (b.type === "submit") { b.disabled = true; b.setAttribute("aria-busy", "true"); }
|
|
36
|
+
});
|
|
37
|
+
}, 0);
|
|
34
38
|
}
|
|
35
39
|
},
|
|
36
40
|
true // capture, so the check runs before any other submit handler
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
// Ticks "Xh YYm left" on [data-hrl-countdown="<iso time>"]
|
|
1
|
+
// Ticks "Xh YYm left" on [data-hrl-countdown="<iso time>"] (data-hrl-done when
|
|
2
|
+
// it runs out) and "Xh YYm so far" on [data-hrl-since]. Display only:
|
|
2
3
|
// the server ends the mode itself, with or without this page open.
|
|
3
4
|
(function () {
|
|
4
5
|
"use strict";
|
|
6
|
+
function hm(min) { return Math.floor(min / 60) + "h " + String(min % 60).padStart(2, "0") + "m"; }
|
|
5
7
|
function tick() {
|
|
6
8
|
document.querySelectorAll("[data-hrl-countdown]").forEach(function (el) {
|
|
7
9
|
var left = Math.max(0, Math.round((Date.parse(el.dataset.hrlCountdown) - Date.now()) / 60000));
|
|
8
|
-
el.textContent = left === 0 ?
|
|
10
|
+
el.textContent = left === 0 ? (el.dataset.hrlDone || "ended") : hm(left) + " left";
|
|
11
|
+
});
|
|
12
|
+
document.querySelectorAll("[data-hrl-since]").forEach(function (el) {
|
|
13
|
+
el.textContent = hm(Math.max(0, Math.floor((Date.now() - Date.parse(el.dataset.hrlSince)) / 60000))) + " so far";
|
|
9
14
|
});
|
|
10
15
|
}
|
|
11
16
|
document.addEventListener("DOMContentLoaded", function () { tick(); setInterval(tick, 30000); });
|
|
17
|
+
// A PWA brought back from the background repaints at once, not on the next tick.
|
|
18
|
+
document.addEventListener("visibilitychange", function () { if (!document.hidden) tick(); });
|
|
12
19
|
})();
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
(function () {
|
|
6
6
|
var dialog = document.querySelector("[data-hrl-search]");
|
|
7
7
|
if (!dialog || !dialog.showModal) return;
|
|
8
|
-
var form = dialog.querySelector("form");
|
|
8
|
+
var form = dialog.querySelector("form[role=search]");
|
|
9
9
|
var input = form.querySelector("input");
|
|
10
10
|
var out = dialog.querySelector("[data-hrl-search-results]");
|
|
11
11
|
var timer;
|
|
@@ -101,8 +101,9 @@ html:has(.hrl-body) { scroll-padding-top: 4rem; scroll-padding-bottom: calc(4.4r
|
|
|
101
101
|
}
|
|
102
102
|
.hrl-shell { display: flex; min-height: 100vh; }
|
|
103
103
|
.hrl-content { flex: 1; min-width: 0; }
|
|
104
|
+
/* Landscape notch: never closer to the cutout than the gutter. */
|
|
104
105
|
.hrl-main {
|
|
105
|
-
padding: .85rem;
|
|
106
|
+
padding: .85rem max(.85rem, env(safe-area-inset-right)) .85rem max(.85rem, env(safe-area-inset-left));
|
|
106
107
|
padding-bottom: calc(4.4rem + env(safe-area-inset-bottom));
|
|
107
108
|
max-width: 68rem;
|
|
108
109
|
margin: 0 auto;
|
|
@@ -242,6 +243,7 @@ html:has(.hrl-body) { scroll-padding-top: 4rem; scroll-padding-bottom: calc(4.4r
|
|
|
242
243
|
.hrl-card--empty { text-align: center; color: var(--hrl-muted); padding: 1.8rem 1rem; }
|
|
243
244
|
.hrl-card--warn { background: var(--hrl-warn-bg); border-color: transparent; }
|
|
244
245
|
.hrl-card > p:first-child { margin-top: 0; }
|
|
246
|
+
.hrl-card > hr { border: 0; border-top: 1px solid var(--hrl-line); margin: var(--hrl-s-4) 0; }
|
|
245
247
|
/* Vertical rhythm: forms, buttons and disclosures stacked in a card never
|
|
246
248
|
touch (only .hrl-field carried spacing, so a button above a form, or an
|
|
247
249
|
open disclosure above the next control, sat flush). 12px >= the 8px
|
|
@@ -281,12 +283,12 @@ details > form > button[type=submit], details > form > .hrl-btn { margin-top: .2
|
|
|
281
283
|
.hrl-btn {
|
|
282
284
|
display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
|
|
283
285
|
min-height: var(--hrl-target); padding: .55rem 1rem; border-radius: var(--hrl-radius-sm);
|
|
284
|
-
border: 1px solid var(--hrl-
|
|
286
|
+
border: 1px solid var(--hrl-field); background: var(--hrl-card); color: var(--hrl-ink);
|
|
285
287
|
font-size: var(--hrl-t-body); font-weight: 600; cursor: pointer; text-decoration: none;
|
|
286
288
|
max-width: 100%; overflow-wrap: anywhere; text-align: center;
|
|
287
289
|
}
|
|
288
290
|
.hrl-btn--primary { background: var(--hrl-accent); border-color: var(--hrl-accent); color: var(--hrl-accent-ink); }
|
|
289
|
-
.hrl-btn--danger { color: var(--hrl-bad); border-color: var(--hrl-bad
|
|
291
|
+
.hrl-btn--danger { color: var(--hrl-bad); border-color: var(--hrl-bad); }
|
|
290
292
|
.hrl-btn--block { width: 100%; }
|
|
291
293
|
.hrl-btn--big { min-height: 58px; font-size: var(--hrl-t-lead); }
|
|
292
294
|
.hrl-btn[disabled] { opacity: .55; cursor: default; }
|
|
@@ -319,6 +321,7 @@ summary.hrl-btn::-webkit-details-marker { display: none; } /* Safari before 18.4
|
|
|
319
321
|
.hrl-table td[data-label=""] > :is(a, button) + :is(a, button, form) { margin-left: var(--hrl-s-2); }
|
|
320
322
|
.hrl-table td[data-label=""] > :is(a, button) + form { display: inline-block; vertical-align: top; } /* a button_to beside a link, not under it */
|
|
321
323
|
.hrl-table td[data-label=""] > form + form { margin-top: var(--hrl-s-2); }
|
|
324
|
+
.hrl-table td form > :is(.hrl-input, select) + :is(.hrl-btn, input[type="submit"]) { margin-top: var(--hrl-s-2); } /* a note above its button, never touching */
|
|
322
325
|
.hrl-table tr:has(.hrl-btn) > td, .hrl-table tr:has(.hrl-btn) td > .hrl-badge { vertical-align: middle; } /* text lines up with a 44px button */
|
|
323
326
|
/* Stack on phones and on the narrow content column beside the 768-1023px rail. */
|
|
324
327
|
@media (max-width: 640px), (min-width: 768px) and (max-width: 1023px) {
|
|
@@ -338,6 +341,7 @@ summary.hrl-btn::-webkit-details-marker { display: none; } /* Safari before 18.4
|
|
|
338
341
|
|
|
339
342
|
/* Forms */
|
|
340
343
|
.hrl-field { margin-bottom: .8rem; }
|
|
344
|
+
@media (min-width: 1024px) { .hrl-field { max-width: 40rem; } }
|
|
341
345
|
.hrl-field label, .hrl-field legend { display: block; font-size: var(--hrl-t-small); font-weight: 600; margin-bottom: .3rem; }
|
|
342
346
|
fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
|
|
343
347
|
.hrl-hint { color: var(--hrl-muted); font-size: var(--hrl-t-small); margin-top: .25rem; }
|
|
@@ -366,7 +370,7 @@ fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
|
|
|
366
370
|
.hrl-field--check label .hrl-hint { display: block; margin-top: 0; font-weight: 400; }
|
|
367
371
|
/* Rails wraps an invalid label/field in this div; contents keeps the stack, the re-pointed token reddens the border. */
|
|
368
372
|
.field_with_errors { display: contents; --hrl-field: var(--hrl-bad); }
|
|
369
|
-
.field_with_errors label { color: var(--hrl-bad); }
|
|
373
|
+
.field_with_errors label, .hrl-hint--bad { color: var(--hrl-bad); }
|
|
370
374
|
.hrl-errors { background: var(--hrl-bad-bg); color: var(--hrl-bad); border-radius: var(--hrl-radius-sm); padding: .7rem .9rem; margin-bottom: .85rem; font-size: var(--hrl-t-body); }
|
|
371
375
|
.hrl-errors ul { margin: .3rem 0 0; padding-left: 1.1rem; }
|
|
372
376
|
.hrl-form-actions { display: flex; flex-wrap: wrap; gap: .6rem; margin-top: 1rem; }
|
|
@@ -381,7 +385,7 @@ fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
|
|
|
381
385
|
min-width: var(--hrl-target); justify-content: center;
|
|
382
386
|
}
|
|
383
387
|
.hrl-chiprow input { position: absolute; opacity: 0; pointer-events: none; }
|
|
384
|
-
.hrl-chiprow input:checked + span, .hrl-chiprow label:has(input:checked) { background: var(--hrl-info-bg); border-color: var(--hrl-accent); color: var(--hrl-accent); }
|
|
388
|
+
.hrl-chiprow input:checked + span, .hrl-chiprow label:has(input:checked), .hrl-btn[aria-current="true"] { background: var(--hrl-info-bg); border-color: var(--hrl-accent); color: var(--hrl-accent); }
|
|
385
389
|
/* The radio is invisible, so its focus ring goes on the chip. */
|
|
386
390
|
.hrl-chiprow label:has(input:focus-visible) { outline: var(--hrl-focus-w) solid var(--hrl-focus); outline-offset: var(--hrl-focus-offset); }
|
|
387
391
|
|
|
@@ -457,10 +461,10 @@ fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
|
|
|
457
461
|
.hrl-tree__name, .hrl-tree__role { min-width: 0; overflow-wrap: anywhere; }
|
|
458
462
|
.hrl-tree__name { font-weight: 700; font-size: var(--hrl-t-small); }
|
|
459
463
|
a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--hrl-target); }
|
|
460
|
-
.hrl-tree__role { color: var(--hrl-
|
|
464
|
+
.hrl-tree__role, .hrl-tree__card .hrl-muted { color: var(--hrl-ink-2); font-size: var(--hrl-t-small); }
|
|
461
465
|
.hrl-chainrow { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
|
|
462
466
|
.hrl-chainrow__step { display: inline-flex; align-items: center; gap: .4rem; min-width: 0; }
|
|
463
|
-
.hrl-chainrow .hrl-badge { white-space: normal; overflow-wrap: anywhere; }
|
|
467
|
+
.hrl-chainrow .hrl-badge, .hrl-badge--type { white-space: normal; overflow-wrap: anywhere; } /* long leave-balance chips wrap, not cut */
|
|
464
468
|
.hrl-chainrow__arrow { color: var(--hrl-muted); }
|
|
465
469
|
|
|
466
470
|
/* Definition list */
|
|
@@ -505,6 +509,7 @@ a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--
|
|
|
505
509
|
.hrl-mgrid__flag { position: absolute; top: 3px; right: 4px; width: 7px; height: 7px; border-radius: var(--hrl-radius-pill); background: var(--hrl-bad); }
|
|
506
510
|
.hrl-legend { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .6rem; }
|
|
507
511
|
.hrl-legend .hrl-mgrid__flag { position: static; display: inline-block; }
|
|
512
|
+
.hrl-legend + .hrl-mgrid { margin-top: .6rem; } /* the month key sits above its grid */
|
|
508
513
|
@media (min-width: 768px) { .hrl-mgrid--cal .hrl-mgrid__day { min-height: 88px; } }
|
|
509
514
|
|
|
510
515
|
/* Utility */
|
|
@@ -579,11 +584,12 @@ a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--
|
|
|
579
584
|
.hrl-idcard__foot b { font: 700 calc(13 * var(--u))/1 var(--hrl-card-display, var(--hrl-font)); color: #fff; }
|
|
580
585
|
.hrl-idcard__foot span { font: 500 calc(10 * var(--u))/1 var(--hrl-card-body, var(--hrl-font)); letter-spacing: .14em; color: rgba(255, 255, 255, .85); }
|
|
581
586
|
.hrl-idcard-actions { display: flex; flex-wrap: wrap; gap: .85rem; align-items: flex-start; justify-content: center; margin-top: 1.5rem; }
|
|
587
|
+
.hrl-idcard-help { flex-basis: 100%; justify-content: center; }
|
|
582
588
|
|
|
583
589
|
/* Print: one CR80 face per page, nothing else. */
|
|
584
590
|
@media print {
|
|
585
591
|
@page { size: 54mm 85.6mm; margin: 0; }
|
|
586
|
-
.hrl-side, .hrl-appbar, .hrl-tabbar, .hrl-flash, .hrl-page__head, .hrl-idcard__label, .hrl-idcard-actions { display: none !important; }
|
|
592
|
+
.hrl-side, .hrl-appbar, .hrl-tabbar, .hrl-flash, .hrl-page__head, .hrl-backlink, .hrl-idcard__label, .hrl-idcard-actions { display: none !important; }
|
|
587
593
|
.hrl-body { background: #fff; }
|
|
588
594
|
.hrl-main { padding: 0; margin: 0; }
|
|
589
595
|
.hrl-idcard-faces { display: block; }
|
|
@@ -619,12 +625,14 @@ a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--
|
|
|
619
625
|
.hrl-fill--offline { fill: var(--hrl-mode-offline); } .hrl-stroke--offline { stroke: var(--hrl-mode-offline); } .hrl-fill-bg--offline { background: var(--hrl-mode-offline); }
|
|
620
626
|
.hrl-chart-row { display: grid; gap: var(--hrl-gap-target); }
|
|
621
627
|
@media (min-width: 481px) { .hrl-chart-row { grid-template-columns: 10rem 1fr; align-items: center; } }
|
|
628
|
+
.hrl-chart-row .hrl-num { white-space: nowrap; } /* "16h 13m" stays on one line at 360px */
|
|
622
629
|
.hrl-donut { width: 10rem; max-width: 100%; transform: rotate(0deg); }
|
|
623
630
|
.hrl-donut__track { stroke: var(--hrl-bg); }
|
|
631
|
+
.hrl-field input[type="color"] { width: 4.5rem; padding: .2rem; }
|
|
624
632
|
.hrl-swatch { display: inline-block; width: .7rem; height: .7rem; border-radius: var(--hrl-radius-pill); vertical-align: middle; }
|
|
625
633
|
.hrl-chart-scroll { overflow-x: auto; }
|
|
626
634
|
/* Wide enough per bar that the height (10rem) sets the scale, so labels render ~12.6px and long ranges scroll. */
|
|
627
|
-
.hrl-bars { width: 100%; min-width: max(16rem, calc(var(--hrl-bars-n, 0) * 1.
|
|
635
|
+
.hrl-bars { width: 100%; min-width: max(16rem, calc(var(--hrl-bars-n, 0) * 1.75rem)); height: 10rem; }
|
|
628
636
|
.hrl-bars__label { font-size: 9px; fill: var(--hrl-muted); }
|
|
629
637
|
.hrl-daystrip { width: 100%; height: 2rem; background: var(--hrl-bg); border-radius: var(--hrl-radius-sm); }
|
|
630
638
|
.hrl-timeline__auto { stroke: var(--hrl-card); stroke-width: .3; stroke-dasharray: .6 .4; }
|
|
@@ -642,5 +650,6 @@ a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--
|
|
|
642
650
|
|
|
643
651
|
/* Search palette (search.js): one box for screens and people, ⌘K anywhere. */
|
|
644
652
|
.hrl-search { width: min(36rem, calc(100% - 2rem)); margin: 10vh auto auto; padding: var(--hrl-s-4); border: 1px solid var(--hrl-line); border-radius: var(--hrl-radius); background: var(--hrl-card); color: var(--hrl-ink); }
|
|
653
|
+
.hrl-search__close { display: flex; justify-content: flex-end; margin: 0 0 var(--hrl-s-2); }
|
|
645
654
|
.hrl-search::backdrop { background: rgb(15 23 42 / .4); }
|
|
646
655
|
.hrl-search__list { list-style: none; margin: var(--hrl-s-1) 0 0; padding: 0; display: grid; gap: var(--hrl-s-2); }
|
|
@@ -104,6 +104,12 @@ module HrLite
|
|
|
104
104
|
alert: "A check-in time is required when a check-out is set."
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
+
# Full-day leave outranks any punch, so the fix would change nothing anyone reads.
|
|
108
|
+
if LeaveRequest.active_on(date).where(user_id: @employee.id, half_day: false).exists?
|
|
109
|
+
return redirect_to admin_attendance_path(@employee.id, date: date, month: date.strftime("%Y-%m")),
|
|
110
|
+
alert: "The day is covered by approved leave — cancel the leave first."
|
|
111
|
+
end
|
|
112
|
+
|
|
107
113
|
record.assign_attributes(attrs)
|
|
108
114
|
record.clear_check_out if params[:reset] == "check_out"
|
|
109
115
|
record.half_day_part = attrs[:half_day_part].presence if attrs.key?(:half_day_part)
|
|
@@ -209,16 +215,17 @@ module HrLite
|
|
|
209
215
|
# whose own page this viewer may open for this person.
|
|
210
216
|
def summary_requests(user, range)
|
|
211
217
|
id, from, to = user.id, range.first, range.last
|
|
218
|
+
own = id == hr_current_user.id # nobody decides their own request: open the employee's own pages
|
|
212
219
|
day = ->(d) { d.strftime("%a %-d %b") }
|
|
213
220
|
{
|
|
214
221
|
"leave.view" => -> {
|
|
215
222
|
LeaveRequest.where(user_id: id).overlapping_range(from, to).includes(:leave_type)
|
|
216
|
-
.map { |r| [ r.leave_type.name, r.date_range_label, r.status, admin_leave_request_path(r) ] } +
|
|
217
|
-
CompOffRequest.where(user_id: id, date_worked: range).map { |r| [ "Comp-off", day.(r.date_worked), r.status, admin_comp_off_request_path(r) ] }
|
|
223
|
+
.map { |r| [ r.leave_type.name, r.date_range_label, r.status, own ? leave_request_path(r) : admin_leave_request_path(r) ] } +
|
|
224
|
+
CompOffRequest.where(user_id: id, date_worked: range).map { |r| [ "Comp-off", day.(r.date_worked), r.status, own ? comp_off_requests_path : admin_comp_off_request_path(r) ] }
|
|
218
225
|
},
|
|
219
226
|
"attendance.view" => -> {
|
|
220
227
|
WfhRequest.where(user_id: id).overlapping(from, to).map { |r| [ "WFH", r.dates_label, r.status, admin_wfh_request_path(r) ] } +
|
|
221
|
-
RegularizationRequest.where(user_id: id, date: range).map { |r| [ "Attendance fix", day.(r.date), r.status, admin_regularization_request_path(r) ] }
|
|
228
|
+
RegularizationRequest.where(user_id: id, date: range).map { |r| [ "Attendance fix", day.(r.date), r.status, own ? regularization_requests_path : admin_regularization_request_path(r) ] }
|
|
222
229
|
},
|
|
223
230
|
"expense.approve" => -> {
|
|
224
231
|
Expense.where(user_id: id, spent_on: range).includes(:category)
|
|
@@ -5,6 +5,7 @@ module HrLite
|
|
|
5
5
|
# Appraisal and promotion rows carry review text in plain columns:
|
|
6
6
|
# ordinary leadership governs people and policy, not pay.
|
|
7
7
|
scope = hr_superadmin? ? AuditLog.recent : AuditLog.recent.outside_money_tier
|
|
8
|
+
scope = scope.where.not("action LIKE ?", "%.viewed") unless params[:views] # page views drown the changes
|
|
8
9
|
@audit_logs = paginate(scope.includes(:actor))
|
|
9
10
|
end
|
|
10
11
|
end
|
|
@@ -72,11 +72,11 @@ module HrLite
|
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def edit
|
|
75
|
-
@profile =
|
|
75
|
+
@profile = others_profile
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
def update
|
|
79
|
-
@profile =
|
|
79
|
+
@profile = others_profile
|
|
80
80
|
if @profile.update(update_profile_params)
|
|
81
81
|
redirect_to admin_employee_path(@profile), notice: "Employee profile updated."
|
|
82
82
|
else
|
|
@@ -87,12 +87,16 @@ module HrLite
|
|
|
87
87
|
# Exit: stamps the date (attendance/payroll clip on it) and revokes
|
|
88
88
|
# access via the host hook. Audited via the profile update.
|
|
89
89
|
def offboard
|
|
90
|
-
@profile =
|
|
90
|
+
@profile = others_profile
|
|
91
91
|
# `.to_date` on a raw param raises Date::Error and 500s; the
|
|
92
92
|
# controller's own parser exists for exactly this.
|
|
93
93
|
exit_date = params[:date_of_exit].present? ? parse_date_param(params[:date_of_exit]) : Date.current
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
ActiveRecord::Base.transaction do
|
|
96
|
+
@profile.update!(date_of_exit: exit_date)
|
|
97
|
+
# HR, Finance, Leadership go with the job; Employee stays so the person still opens their slips.
|
|
98
|
+
RoleAssignment.for_user(@profile.user).joins(:role).where.not(hr_lite_roles: { name: Role::EMPLOYEE }).destroy_all
|
|
99
|
+
end
|
|
96
100
|
# The last day has a list too — and it is the one that matters most,
|
|
97
101
|
# since an unreturned laptop and un-revoked access both outlive the
|
|
98
102
|
# employment.
|
|
@@ -110,6 +114,9 @@ module HrLite
|
|
|
110
114
|
|
|
111
115
|
private
|
|
112
116
|
|
|
117
|
+
# Nobody edits or offboards their own record here (auto-approve would publish it); someone else does.
|
|
118
|
+
def others_profile = EmployeeProfile.find(params[:id]).tap { |p| raise ActiveRecord::RecordNotFound if p.user_id == hr_current_user.id }
|
|
119
|
+
|
|
113
120
|
def profile_params
|
|
114
121
|
params.require(:employee_profile).permit(
|
|
115
122
|
:user_id, :manager_id, :designation, :date_of_birth, :date_of_joining,
|
|
@@ -15,6 +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
20
|
end
|
|
19
21
|
|
|
20
22
|
def reject
|
|
@@ -24,6 +26,8 @@ module HrLite
|
|
|
24
26
|
|
|
25
27
|
expense.reject!(actor: hr_current_user, note: note)
|
|
26
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."
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
def reimburse
|
|
@@ -20,6 +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
25
|
end
|
|
24
26
|
|
|
25
27
|
def resolve
|
|
@@ -28,6 +30,8 @@ module HrLite
|
|
|
28
30
|
redirect_to admin_hr_requests_path, notice: "Answered."
|
|
29
31
|
rescue ArgumentError
|
|
30
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."
|
|
31
35
|
end
|
|
32
36
|
|
|
33
37
|
private
|
|
@@ -24,7 +24,10 @@ module HrLite
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def create
|
|
27
|
-
|
|
27
|
+
typed = params[:employee_code].to_s.strip
|
|
28
|
+
# The code, else a typed name — only when exactly one person has it.
|
|
29
|
+
profile = recordable.find_by(employee_code: typed) ||
|
|
30
|
+
recordable.select { |p| HrLite.display_name(p.user).casecmp?(typed) }.then { |m| m.first if m.one? }
|
|
28
31
|
@request = LeaveRequest.new(params.require(:leave_request).permit(:leave_type_id, :start_date, :end_date, :half_day, :half_day_part, :reason)
|
|
29
32
|
.merge(user_id: profile&.user_id, created_by_id: hr_current_user.id))
|
|
30
33
|
recorded = profile && LeaveRequest.transaction do
|
|
@@ -18,7 +18,7 @@ module HrLite
|
|
|
18
18
|
month = parse_month_param(params.dig(:payroll_run, :period_month))
|
|
19
19
|
@run = PayrollRun.new(period_month: month, created_by_id: hr_current_user.id,
|
|
20
20
|
notes: params.dig(:payroll_run, :notes))
|
|
21
|
-
if @run.save
|
|
21
|
+
if PayrollRun.transaction { @run.save && audit!("payroll.created", @run) }
|
|
22
22
|
redirect_to admin_payroll_run_path(@run), notice: "Run created — compute when ready."
|
|
23
23
|
else
|
|
24
24
|
render :new, status: :unprocessable_entity
|
|
@@ -32,7 +32,7 @@ module HrLite
|
|
|
32
32
|
|
|
33
33
|
def destroy
|
|
34
34
|
run = PayrollRun.find(params[:id])
|
|
35
|
-
if run.destroy
|
|
35
|
+
if PayrollRun.transaction { run.destroy && audit!("payroll.deleted", run) }
|
|
36
36
|
redirect_to admin_payroll_runs_path, notice: "Draft run deleted.", status: :see_other
|
|
37
37
|
else
|
|
38
38
|
redirect_to admin_payroll_run_path(run), alert: run.errors.full_messages.to_sentence,
|
|
@@ -41,7 +41,7 @@ module HrLite
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def compute
|
|
44
|
-
transition { |run| run.
|
|
44
|
+
transition { |run| run.compute_later!(actor: hr_current_user) && "Compute started." }
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def approve
|
|
@@ -77,6 +77,8 @@ module HrLite
|
|
|
77
77
|
|
|
78
78
|
private
|
|
79
79
|
|
|
80
|
+
def audit!(action, run) = AuditLog.record!(action: action, subject: run, actor: hr_current_user, changes: { "period" => run.label })
|
|
81
|
+
|
|
80
82
|
def require_approver_or_money!
|
|
81
83
|
return if hr_can?("payroll.manage", scope: :all)
|
|
82
84
|
|
|
@@ -99,8 +101,10 @@ module HrLite
|
|
|
99
101
|
CSV.generate do |csv|
|
|
100
102
|
csv << [ "Code", "Name", "Days", "LOP", "Gross", "PF", "ESI", "PT", "TDS",
|
|
101
103
|
"Net pay", "Bank", "Account", "IFSC" ]
|
|
102
|
-
run.salary_slips.includes(:user).
|
|
103
|
-
|
|
104
|
+
slips = run.salary_slips.includes(:user).to_a
|
|
105
|
+
profiles = EmployeeProfile.where(user_id: slips.map(&:user_id)).index_by(&:user_id) # one query, not one a slip
|
|
106
|
+
slips.each do |slip|
|
|
107
|
+
profile = profiles[slip.user_id]
|
|
104
108
|
deductions = slip.deductions_rows.index_by { |row| row["code"] }
|
|
105
109
|
csv << [
|
|
106
110
|
profile&.employee_code, HrLite.display_name(slip.user),
|
|
@@ -109,9 +109,10 @@ module HrLite
|
|
|
109
109
|
visible_users.flat_map do |user|
|
|
110
110
|
types.map do |type|
|
|
111
111
|
balance = LeaveBalance.for(user, type, year)
|
|
112
|
+
entitled = balance.entitled
|
|
113
|
+
used = balance.used # each once: both query
|
|
112
114
|
{ "Employee" => HrLite.display_name(user), "Type" => type.code,
|
|
113
|
-
"Entitled" =>
|
|
114
|
-
"Available" => balance.available.to_f }
|
|
115
|
+
"Entitled" => entitled.to_f, "Used" => used.to_f, "Available" => (entitled - used).to_f }
|
|
115
116
|
end
|
|
116
117
|
end
|
|
117
118
|
end
|
|
@@ -45,7 +45,7 @@ module HrLite
|
|
|
45
45
|
|
|
46
46
|
AuditLog.record!(action: "tax_proof.downloaded", subject: declaration, actor: hr_current_user,
|
|
47
47
|
changes: { "file" => file.filename.to_s, "owner" => HrLite.display_name(declaration.user) })
|
|
48
|
-
redirect_to Rails.application.routes.url_helpers.rails_blob_path(file, disposition: "attachment", only_path: true),
|
|
48
|
+
redirect_to Rails.application.routes.url_helpers.rails_blob_path(file, disposition: "attachment", only_path: true, expires_in: 5.minutes),
|
|
49
49
|
allow_other_host: false
|
|
50
50
|
end
|
|
51
51
|
|
|
@@ -57,6 +57,8 @@ module HrLite
|
|
|
57
57
|
declaration.tax_declaration_items.each do |item|
|
|
58
58
|
item.update!(verified_amount: item.declared_amount) if item.verified_amount.nil?
|
|
59
59
|
end
|
|
60
|
+
AuditLog.record!(action: "tax_declaration.accepted_all", subject: declaration, actor: hr_current_user,
|
|
61
|
+
changes: { "owner" => HrLite.display_name(declaration.user) })
|
|
60
62
|
declaration.verify!(actor: hr_current_user, note: "Proof accepted")
|
|
61
63
|
end
|
|
62
64
|
redirect_to admin_tax_declarations_path, notice: "Accepted and verified — payroll uses these amounts now."
|
|
@@ -66,9 +68,16 @@ module HrLite
|
|
|
66
68
|
|
|
67
69
|
def update
|
|
68
70
|
@declaration = find_manageable
|
|
71
|
+
return redirect_to admin_tax_declaration_path(@declaration), alert: "Only a submitted declaration can be checked." unless @declaration.submitted?
|
|
72
|
+
|
|
69
73
|
# Recording what the proof supported is a separate act from deciding
|
|
70
74
|
# — an amount can be corrected while the decision is still pending.
|
|
71
|
-
|
|
75
|
+
recorded = TaxDeclaration.transaction do
|
|
76
|
+
@declaration.update(verification_params) &&
|
|
77
|
+
AuditLog.record!(action: "tax_declaration.amounts_recorded", subject: @declaration, actor: hr_current_user,
|
|
78
|
+
changes: { "owner" => HrLite.display_name(@declaration.user) })
|
|
79
|
+
end
|
|
80
|
+
if recorded
|
|
72
81
|
return verify if params[:verify] # "Record and verify": the typed amounts are saved first
|
|
73
82
|
redirect_to admin_tax_declaration_path(@declaration), notice: "Amounts recorded."
|
|
74
83
|
else
|
|
@@ -82,7 +91,8 @@ module HrLite
|
|
|
82
91
|
declaration.verify!(actor: hr_current_user, note: params[:note])
|
|
83
92
|
redirect_to admin_tax_declarations_path, notice: "Verified — payroll will use the checked amounts."
|
|
84
93
|
rescue ActiveRecord::RecordInvalid
|
|
85
|
-
redirect_to admin_tax_declaration_path(declaration),
|
|
94
|
+
redirect_to admin_tax_declaration_path(declaration),
|
|
95
|
+
alert: declaration.errors.full_messages.first || "Only a submitted declaration can be verified."
|
|
86
96
|
end
|
|
87
97
|
|
|
88
98
|
def reject
|
|
@@ -9,6 +9,7 @@ module HrLite
|
|
|
9
9
|
class ApprovalsController < ApplicationController
|
|
10
10
|
def index
|
|
11
11
|
@approvals = paginate(Approval.pending_for(hr_current_user).includes(:step, :subject).order(:created_at))
|
|
12
|
+
@in_flow = Approval.pending_for(hr_current_user).pluck(:subject_type, :subject_id) # their queue rows would repeat them
|
|
12
13
|
@delegations = ApprovalDelegation.live_on(Date.current)
|
|
13
14
|
.where(from_user_id: hr_current_user.id)
|
|
14
15
|
.includes(:to_user)
|
|
@@ -3,12 +3,12 @@ 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
|
-
@out_week = LeaveRequest.approved.overlapping_range(Date.current
|
|
6
|
+
@out_week = LeaveRequest.approved.overlapping_range(Date.current, Date.current.end_of_week)
|
|
7
7
|
.includes(:user).where(user_id: HrLite.active_employees.map(&:id))
|
|
8
8
|
.sort_by { |leave| hr_display_name(leave.user).downcase }
|
|
9
9
|
@attention = attention_days
|
|
10
10
|
@waiting = Approval.pending_for(hr_current_user).count
|
|
11
|
-
@mine_pending = [ LeaveRequest.includes(:leave_type), RegularizationRequest, CompOffRequest ]
|
|
11
|
+
@mine_pending = [ LeaveRequest.includes(:leave_type), RegularizationRequest, CompOffRequest, WfhRequest ]
|
|
12
12
|
.flat_map { |scope| scope.pending.where(user_id: hr_current_user.id).order(created_at: :desc).limit(5) }
|
|
13
13
|
.max_by(5, &:created_at)
|
|
14
14
|
end
|
|
@@ -22,19 +22,19 @@ module HrLite
|
|
|
22
22
|
def cancel
|
|
23
23
|
request = RegularizationRequest.where(user_id: hr_current_user.id).find(params[:id])
|
|
24
24
|
request.cancel!(actor: hr_current_user)
|
|
25
|
-
redirect_to regularization_requests_path, notice: "
|
|
25
|
+
redirect_to regularization_requests_path, notice: "Day fix cancelled."
|
|
26
26
|
rescue ActiveRecord::RecordInvalid
|
|
27
|
-
redirect_to regularization_requests_path, alert: "Only pending
|
|
27
|
+
redirect_to regularization_requests_path, alert: "Only pending day fixes can be cancelled."
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
private
|
|
31
31
|
|
|
32
32
|
def create_notice
|
|
33
33
|
rule = HrLite.config.self_regularization
|
|
34
|
-
return "
|
|
34
|
+
return "Sent for approval — your manager or HR will review it." unless rule
|
|
35
35
|
return "Fixed. #{helpers.pluralize(@request.self_fixes_left, 'self-fix')} left this week." if @request.approved?
|
|
36
36
|
|
|
37
|
-
"Sent
|
|
37
|
+
"Sent for approval — you can fix only a missed punch or short day on a working day in the last #{rule[:within_days]} days yourself, " \
|
|
38
38
|
"#{rule[:per_week]} times a week."
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -28,10 +28,11 @@ module HrLite
|
|
|
28
28
|
TaxDeclaration.new(user_id: hr_current_user.id,
|
|
29
29
|
financial_year: financial_year, regime: employee_regime)
|
|
30
30
|
# A verified declaration is what payroll is deducting against; changing
|
|
31
|
-
# it would silently re-open a decision HR has already made.
|
|
32
|
-
|
|
31
|
+
# it would silently re-open a decision HR has already made. A submitted
|
|
32
|
+
# one is what HR is checking, so it cannot shift under them either.
|
|
33
|
+
if @declaration.verified? || @declaration.submitted?
|
|
33
34
|
return redirect_to tax_declaration_path,
|
|
34
|
-
alert: "This year's declaration is
|
|
35
|
+
alert: "This year's declaration is #{@declaration.status}. Ask HR to reopen it."
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
if @declaration.update(declaration_params)
|
|
@@ -59,10 +60,13 @@ module HrLite
|
|
|
59
60
|
|
|
60
61
|
private
|
|
61
62
|
|
|
63
|
+
# This year or last (late proof); any other year falls back to this one.
|
|
62
64
|
def financial_year
|
|
63
|
-
FinancialYear.start_for(
|
|
65
|
+
current = FinancialYear.start_for(Date.current)
|
|
66
|
+
asked = FinancialYear.start_for(Date.parse(params[:financial_year].to_s))
|
|
67
|
+
asked.between?(current.prev_year, current) ? asked : current
|
|
64
68
|
rescue Date::Error
|
|
65
|
-
|
|
69
|
+
current
|
|
66
70
|
end
|
|
67
71
|
|
|
68
72
|
def current_declaration
|