hr_lite 0.53.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 +73 -0
- data/README.md +5 -2
- 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/emoji_pick.js +4 -4
- data/app/assets/javascripts/hr_lite/search.js +1 -1
- data/app/assets/stylesheets/hr_lite/hr_lite.css +24 -14
- data/app/controllers/hr_lite/admin/attendances_controller.rb +12 -3
- data/app/controllers/hr_lite/admin/audit_logs_controller.rb +1 -0
- data/app/controllers/hr_lite/admin/comp_off_requests_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/documents_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/employees_controller.rb +11 -4
- data/app/controllers/hr_lite/admin/expenses_controller.rb +17 -1
- data/app/controllers/hr_lite/admin/hr_requests_controller.rb +4 -0
- data/app/controllers/hr_lite/admin/leave_requests_controller.rb +5 -2
- data/app/controllers/hr_lite/admin/payroll_runs_controller.rb +9 -5
- data/app/controllers/hr_lite/admin/regularization_requests_controller.rb +1 -1
- 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/statutory_rate_cards_controller.rb +2 -1
- data/app/controllers/hr_lite/admin/tax_declarations_controller.rb +15 -4
- data/app/controllers/hr_lite/admin/work_locations_controller.rb +1 -1
- data/app/controllers/hr_lite/approvals_controller.rb +1 -0
- data/app/controllers/hr_lite/home_controller.rb +2 -11
- data/app/controllers/hr_lite/loans_controller.rb +1 -1
- 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 +34 -9
- 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 +21 -4
- 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 +2 -1
- data/app/models/hr_lite/statutory_rate_card_record.rb +6 -0
- 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/index.html.erb +4 -4
- data/app/views/hr_lite/admin/attendances/show.html.erb +4 -2
- 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/benefits/index.html.erb +10 -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 +6 -6
- data/app/views/hr_lite/admin/designation_changes/new.html.erb +4 -3
- data/app/views/hr_lite/admin/documents/index.html.erb +9 -4
- 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 +40 -33
- data/app/views/hr_lite/admin/expense_categories/index.html.erb +1 -1
- data/app/views/hr_lite/admin/expenses/index.html.erb +7 -4
- data/app/views/hr_lite/admin/holidays/index.html.erb +7 -5
- data/app/views/hr_lite/admin/hr_requests/index.html.erb +3 -3
- data/app/views/hr_lite/admin/hr_requests/show.html.erb +5 -4
- 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 +6 -4
- data/app/views/hr_lite/admin/leave_types/edit.html.erb +4 -4
- data/app/views/hr_lite/admin/leave_types/index.html.erb +5 -5
- data/app/views/hr_lite/admin/loans/index.html.erb +1 -1
- data/app/views/hr_lite/admin/loans/new.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/office_locations/index.html.erb +1 -1
- data/app/views/hr_lite/admin/overview/index.html.erb +10 -31
- data/app/views/hr_lite/admin/payroll_line_items/new.html.erb +2 -1
- data/app/views/hr_lite/admin/payroll_runs/index.html.erb +3 -3
- data/app/views/hr_lite/admin/payroll_runs/show.html.erb +15 -10
- data/app/views/hr_lite/admin/performance_plans/_form.html.erb +2 -1
- data/app/views/hr_lite/admin/policies/index.html.erb +2 -2
- data/app/views/hr_lite/admin/policies/new.html.erb +4 -3
- data/app/views/hr_lite/admin/regularization_requests/index.html.erb +3 -4
- data/app/views/hr_lite/admin/regularization_requests/show.html.erb +5 -6
- 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 +8 -6
- data/app/views/hr_lite/admin/roles/edit.html.erb +4 -2
- data/app/views/hr_lite/admin/roles/index.html.erb +1 -1
- 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 +14 -10
- 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 +8 -4
- data/app/views/hr_lite/admin/wfh_requests/index.html.erb +17 -16
- data/app/views/hr_lite/admin/wfh_requests/show.html.erb +3 -5
- 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 +5 -4
- data/app/views/hr_lite/attendance/_custom_status.html.erb +7 -9
- 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 +10 -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 +7 -5
- 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 +6 -5
- data/app/views/hr_lite/id_cards/show.html.erb +5 -1
- data/app/views/hr_lite/leave_requests/_balance_chips.html.erb +3 -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/loans/index.html.erb +1 -1
- 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/resignations/show.html.erb +2 -1
- data/app/views/hr_lite/salary_slips/_slip_detail.html.erb +7 -7
- data/app/views/hr_lite/salary_slips/pdf.html.erb +2 -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 +5 -4
- 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/app/views/layouts/hr_lite/mailer.html.erb +10 -2
- data/app/views/layouts/hr_lite/mailer.text.erb +4 -1
- data/config/routes.rb +1 -1
- data/lib/hr_lite/amount_in_words.rb +7 -5
- data/lib/hr_lite/notifications.rb +5 -2
- data/lib/hr_lite/statutory_seeds.rb +1 -1
- 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,79 @@ 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
|
+
|
|
70
|
+
## [0.54.0] - 2026-10-17
|
|
71
|
+
|
|
72
|
+
Fixes from the HRMS UI/UX audit of 0.52.0. No migrations.
|
|
73
|
+
|
|
74
|
+
- Approvals: Home, the HR overview and the Inbox badge show one count. Decision lists leave out your own requests, which used to open a blank 404. "Review requests" opens Inbox › All, and each row there names the request ("Sick leave, 30 Oct (half day) · 0.5 days") and opens it. Home locations opens on Pending. Every reject reason is required before the form submits.
|
|
75
|
+
- Confirms before one-way actions: resigning, accepting a resignation, saving a PIP, giving someone a role (the picker now starts blank and filters by name), changing a role's permissions, publishing a policy ("Publish and notify 4 people by email?"), recomputing a run that has an approval, approving past blocking warnings, ending health cover, and accepting all tax claims (names the claims without proof).
|
|
76
|
+
- Money: payroll run totals no longer wrap mid-number on a phone. "Employer cost" reads "Employer contributions". The payslip spells the paise its figure shows. The earnings table stacks on phones. Tax-working amounts stay on one line. My loans totals active loans only. Leave chips show days pending.
|
|
77
|
+
- Phones: tab-row links, list Edit/Open links and the fix-a-punch links are 44px targets. Work-time chart labels are readable at any range. The tax-slab editor is a labelled three-column grid. The bulk holiday box has a label. Employee pickers on pay items, loans and benefits start blank, are required and filter by name.
|
|
78
|
+
- Attendance: the check-in card says "Check in by 10:05" and what lateness costs. A day counts its penalty minutes when deciding whether it was short, so the card no longer says "day done" under "short by 20m". The status picker offers word quick picks instead of emoji. Team attendance shows leave, holidays and weekly offs instead of "No punch".
|
|
79
|
+
- Tax and rate cards: "Record and verify" saves the amounts on screen before verifying. A new financial year defaults to 1 April. Shipped rate cards still count as verified, but the sign-off reads "Shipped defaults, not checked by a person".
|
|
80
|
+
- Admin: approvers can open a claim's receipt (permission-checked, audited, 5-minute link). Working-day settings have their own HR settings tab. New run shows only to those who can run payroll.
|
|
81
|
+
- Branding: HR emails show `company[:email_logo_url]` as an image and a "Questions:" line from `phone`, `email` and `website`. The slip PDF prints the same contact line, and its cache key is versioned so cached slips are re-rendered. Hosts pass the new keys in `config.company`; without `email_logo_url` an email has no logo image, as before.
|
|
82
|
+
|
|
10
83
|
## [0.53.0] - 2026-10-16
|
|
11
84
|
|
|
12
85
|
- HR runs inside the host's iOS and Android staff apps: pages opened by a Hotwire Native shell load Turbo (vendored 8.0.23) with `turbo-visit-control: reload`, so every visit is a full page load and the engine's plain scripts run exactly as in a browser. Browsers still get no Turbo. `config.native_app = ->(request) { … }` lets the host say which requests are its app; the default matches the Hotwire Native user agent.
|
data/README.md
CHANGED
|
@@ -132,9 +132,12 @@ HrLite.configure do |c|
|
|
|
132
132
|
c.mailer_from = "hr@example.com"
|
|
133
133
|
c.public_url_base = "https://hr.example.com" # enables deep links in emails
|
|
134
134
|
# brand + phone are optional; the ID card prints brand, and its QR holds
|
|
135
|
-
# brand, address and phone as plain text.
|
|
135
|
+
# brand, address and phone as plain text. Emails print email_logo_url (an
|
|
136
|
+
# absolute https URL: mail clients drop data: URIs) and, with the slip PDF,
|
|
137
|
+
# a contact line from phone, email and website.
|
|
136
138
|
c.company = -> { { name: "Acme", brand: "acme.in", address: "Head office address",
|
|
137
|
-
phone: "+91 00000 00000",
|
|
139
|
+
phone: "+91 00000 00000", email: "hr@acme.in", website: "acme.in",
|
|
140
|
+
logo_url: nil, email_logo_url: "https://acme.in/logo.png" } }
|
|
138
141
|
|
|
139
142
|
# Salary-slip PDFs: plug your renderer, or add wicked_pdf to the bundle and
|
|
140
143
|
# the built-in renderer takes over. With neither, PDF export is disabled.
|
|
@@ -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
|
})();
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Quick picks for the status form: a tap puts that status in the text box.
|
|
2
2
|
(function () {
|
|
3
3
|
"use strict";
|
|
4
4
|
document.addEventListener("click", function (e) {
|
|
5
|
-
var b = e.target.closest && e.target.closest("[data-hrl-
|
|
5
|
+
var b = e.target.closest && e.target.closest("[data-hrl-pick]");
|
|
6
6
|
if (!b) return;
|
|
7
|
-
var input = b.closest("form").querySelector("input[name=
|
|
8
|
-
input.value = b.dataset.
|
|
7
|
+
var input = b.closest("form").querySelector("input[name=text]");
|
|
8
|
+
input.value = b.dataset.hrlPick;
|
|
9
9
|
input.focus();
|
|
10
10
|
});
|
|
11
11
|
})();
|
|
@@ -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;
|
|
@@ -124,7 +125,7 @@ html:has(.hrl-body) { scroll-padding-top: 4rem; scroll-padding-bottom: calc(4.4r
|
|
|
124
125
|
.hrl-nav__link--active .hrl-nav__hint { color: inherit; }
|
|
125
126
|
/* Tab row: the current place's tabs on one line; it scrolls sideways, never the page. */
|
|
126
127
|
.hrl-tabrow { display: flex; gap: var(--hrl-gap-target); overflow-x: auto; margin: 0 0 var(--hrl-s-4); padding-bottom: var(--hrl-s-2); border-bottom: 1px solid var(--hrl-line); }
|
|
127
|
-
.hrl-tabrow .hrl-nav__link { flex: 0 0 auto; white-space: nowrap; }
|
|
128
|
+
.hrl-tabrow .hrl-nav__link { flex: 0 0 auto; white-space: nowrap; min-width: var(--hrl-target); justify-content: center; }
|
|
128
129
|
.hrl-side__divider { height: 1px; background: var(--hrl-line); margin: var(--hrl-s-2) var(--hrl-s-1); }
|
|
129
130
|
|
|
130
131
|
/* Mobile app bar */
|
|
@@ -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
|
|
|
@@ -415,12 +419,14 @@ fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
|
|
|
415
419
|
.hrl-card--private .hrl-badge--muted { background: var(--hrl-card); }
|
|
416
420
|
|
|
417
421
|
/* Tax: the two regimes side by side, then the working as a plain table. */
|
|
422
|
+
.hrl-slabs { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--hrl-gap-target); align-items: end; }
|
|
418
423
|
.hrl-tax-compare { display: grid; grid-template-columns: 1fr 1fr; gap: var(--hrl-s-3); margin: var(--hrl-s-4) 0 var(--hrl-s-2); }
|
|
419
424
|
.hrl-tax-compare__col { border: 1px solid var(--hrl-line); border-radius: var(--hrl-radius-sm); padding: var(--hrl-s-3); min-width: 0; }
|
|
420
425
|
.hrl-tax-compare__col--chosen { border: 2px solid var(--hrl-accent); }
|
|
421
426
|
.hrl-tax-compare__amount { font-size: var(--hrl-t-title); font-weight: 700; margin-top: var(--hrl-s-1); overflow-wrap: anywhere; }
|
|
422
427
|
.hrl-tax-working { margin-top: var(--hrl-s-3); }
|
|
423
|
-
.hrl-tax-working__table td { vertical-align: top; overflow-wrap:
|
|
428
|
+
.hrl-tax-working__table td { vertical-align: top; overflow-wrap: break-word; } /* anywhere let the table squeeze amounts mid-number */
|
|
429
|
+
.hrl-tax-working__table td.hrl-num { white-space: nowrap; }
|
|
424
430
|
.hrl-tax-working__total td { font-weight: 700; border-top: 1px solid var(--hrl-line); }
|
|
425
431
|
.hrl-tax-cap { display: block; width: 100%; height: 8px; margin-top: var(--hrl-s-1); accent-color: var(--hrl-accent); }
|
|
426
432
|
|
|
@@ -455,10 +461,10 @@ fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
|
|
|
455
461
|
.hrl-tree__name, .hrl-tree__role { min-width: 0; overflow-wrap: anywhere; }
|
|
456
462
|
.hrl-tree__name { font-weight: 700; font-size: var(--hrl-t-small); }
|
|
457
463
|
a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--hrl-target); }
|
|
458
|
-
.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); }
|
|
459
465
|
.hrl-chainrow { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
|
|
460
466
|
.hrl-chainrow__step { display: inline-flex; align-items: center; gap: .4rem; min-width: 0; }
|
|
461
|
-
.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 */
|
|
462
468
|
.hrl-chainrow__arrow { color: var(--hrl-muted); }
|
|
463
469
|
|
|
464
470
|
/* Definition list */
|
|
@@ -503,6 +509,7 @@ a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--
|
|
|
503
509
|
.hrl-mgrid__flag { position: absolute; top: 3px; right: 4px; width: 7px; height: 7px; border-radius: var(--hrl-radius-pill); background: var(--hrl-bad); }
|
|
504
510
|
.hrl-legend { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .6rem; }
|
|
505
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 */
|
|
506
513
|
@media (min-width: 768px) { .hrl-mgrid--cal .hrl-mgrid__day { min-height: 88px; } }
|
|
507
514
|
|
|
508
515
|
/* Utility */
|
|
@@ -577,11 +584,12 @@ a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--
|
|
|
577
584
|
.hrl-idcard__foot b { font: 700 calc(13 * var(--u))/1 var(--hrl-card-display, var(--hrl-font)); color: #fff; }
|
|
578
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); }
|
|
579
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; }
|
|
580
588
|
|
|
581
589
|
/* Print: one CR80 face per page, nothing else. */
|
|
582
590
|
@media print {
|
|
583
591
|
@page { size: 54mm 85.6mm; margin: 0; }
|
|
584
|
-
.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; }
|
|
585
593
|
.hrl-body { background: #fff; }
|
|
586
594
|
.hrl-main { padding: 0; margin: 0; }
|
|
587
595
|
.hrl-idcard-faces { display: block; }
|
|
@@ -617,12 +625,15 @@ a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--
|
|
|
617
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); }
|
|
618
626
|
.hrl-chart-row { display: grid; gap: var(--hrl-gap-target); }
|
|
619
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 */
|
|
620
629
|
.hrl-donut { width: 10rem; max-width: 100%; transform: rotate(0deg); }
|
|
621
630
|
.hrl-donut__track { stroke: var(--hrl-bg); }
|
|
631
|
+
.hrl-field input[type="color"] { width: 4.5rem; padding: .2rem; }
|
|
622
632
|
.hrl-swatch { display: inline-block; width: .7rem; height: .7rem; border-radius: var(--hrl-radius-pill); vertical-align: middle; }
|
|
623
633
|
.hrl-chart-scroll { overflow-x: auto; }
|
|
624
|
-
|
|
625
|
-
.hrl-
|
|
634
|
+
/* Wide enough per bar that the height (10rem) sets the scale, so labels render ~12.6px and long ranges scroll. */
|
|
635
|
+
.hrl-bars { width: 100%; min-width: max(16rem, calc(var(--hrl-bars-n, 0) * 1.75rem)); height: 10rem; }
|
|
636
|
+
.hrl-bars__label { font-size: 9px; fill: var(--hrl-muted); }
|
|
626
637
|
.hrl-daystrip { width: 100%; height: 2rem; background: var(--hrl-bg); border-radius: var(--hrl-radius-sm); }
|
|
627
638
|
.hrl-timeline__auto { stroke: var(--hrl-card); stroke-width: .3; stroke-dasharray: .6 .4; }
|
|
628
639
|
.hrl-timeline__axis { display: flex; justify-content: space-between; }
|
|
@@ -630,8 +641,6 @@ a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--
|
|
|
630
641
|
.hrl-chart-tip { position: absolute; z-index: 30; max-width: 16rem; padding: .4rem .6rem; border-radius: var(--hrl-radius-sm); background: var(--hrl-ink); color: var(--hrl-card); font-size: var(--hrl-t-small); pointer-events: none; }
|
|
631
642
|
[data-hrl-tip]:focus { outline: 2px solid var(--hrl-accent); outline-offset: 1px; }
|
|
632
643
|
.hrl-custom-status { display: inline-flex; gap: .3rem; align-items: center; overflow-wrap: anywhere; }
|
|
633
|
-
.hrl-emoji-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(2.75rem, 1fr)); gap: var(--hrl-gap-target); margin-top: var(--hrl-gap-target); }
|
|
634
|
-
.hrl-emoji { min-height: var(--hrl-target); border: 1px solid var(--hrl-line); border-radius: var(--hrl-radius-sm); background: var(--hrl-card); font-size: 1.25rem; cursor: pointer; }
|
|
635
644
|
/* Today's check-ins: one initials badge per person; hover/tap shows times. */
|
|
636
645
|
.hrl-checkins + .hrl-checkins { margin-top: .6rem; }
|
|
637
646
|
.hrl-checkins__people { display: flex; flex-wrap: wrap; gap: var(--hrl-gap-target); margin: .4rem 0 0; padding: 0; list-style: none; }
|
|
@@ -641,5 +650,6 @@ a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--
|
|
|
641
650
|
|
|
642
651
|
/* Search palette (search.js): one box for screens and people, ⌘K anywhere. */
|
|
643
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); }
|
|
644
654
|
.hrl-search::backdrop { background: rgb(15 23 42 / .4); }
|
|
645
655
|
.hrl-search__list { list-style: none; margin: var(--hrl-s-1) 0 0; padding: 0; display: grid; gap: var(--hrl-s-2); }
|
|
@@ -11,6 +11,8 @@ module HrLite
|
|
|
11
11
|
@employees = @employees.select { |user| visible.include?(user.id) } if visible
|
|
12
12
|
@records = AttendanceRecord.for_date(@date).where(user_id: @employees.map(&:id)).index_by(&:user_id)
|
|
13
13
|
@flagged_count = @records.values.count(&:flagged?)
|
|
14
|
+
# Leave, holidays and weekly offs are not "No punch": the team board's day status says which.
|
|
15
|
+
@days = TeamDay.new(date: @date).rows.index_by { |row| row.user.id }
|
|
14
16
|
@penalty_minutes = AttendancePenalty.held_minutes(@employees.map(&:id))
|
|
15
17
|
# "How many people punched where": off-site punches count as one place.
|
|
16
18
|
@offices = OfficeLocation.active.to_a
|
|
@@ -102,6 +104,12 @@ module HrLite
|
|
|
102
104
|
alert: "A check-in time is required when a check-out is set."
|
|
103
105
|
end
|
|
104
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
|
+
|
|
105
113
|
record.assign_attributes(attrs)
|
|
106
114
|
record.clear_check_out if params[:reset] == "check_out"
|
|
107
115
|
record.half_day_part = attrs[:half_day_part].presence if attrs.key?(:half_day_part)
|
|
@@ -207,16 +215,17 @@ module HrLite
|
|
|
207
215
|
# whose own page this viewer may open for this person.
|
|
208
216
|
def summary_requests(user, range)
|
|
209
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
|
|
210
219
|
day = ->(d) { d.strftime("%a %-d %b") }
|
|
211
220
|
{
|
|
212
221
|
"leave.view" => -> {
|
|
213
222
|
LeaveRequest.where(user_id: id).overlapping_range(from, to).includes(:leave_type)
|
|
214
|
-
.map { |r| [ r.leave_type.name, r.date_range_label, r.status, admin_leave_request_path(r) ] } +
|
|
215
|
-
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) ] }
|
|
216
225
|
},
|
|
217
226
|
"attendance.view" => -> {
|
|
218
227
|
WfhRequest.where(user_id: id).overlapping(from, to).map { |r| [ "WFH", r.dates_label, r.status, admin_wfh_request_path(r) ] } +
|
|
219
|
-
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) ] }
|
|
220
229
|
},
|
|
221
230
|
"expense.approve" => -> {
|
|
222
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
|
|
@@ -4,7 +4,7 @@ module HrLite
|
|
|
4
4
|
def index
|
|
5
5
|
@status = params[:status].presence_in(CompOffRequest::STATUSES) || "pending"
|
|
6
6
|
@requests = paginate(hr_scope(CompOffRequest.includes(:user).where(status: @status).recent_first,
|
|
7
|
-
"leave.view"))
|
|
7
|
+
"leave.view").where.not(user_id: hr_current_user.id))
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def show
|
|
@@ -11,7 +11,7 @@ module HrLite
|
|
|
11
11
|
@category = params[:category].presence
|
|
12
12
|
scope = hr_scope(Document.includes(:user).recent_first, "document.view")
|
|
13
13
|
scope = scope.where(category: @category) if @category
|
|
14
|
-
@pending = scope.where(verification: "pending").limit(50).to_a
|
|
14
|
+
@pending = scope.where(verification: "pending").where.not(user_id: hr_current_user.id).limit(50).to_a
|
|
15
15
|
@documents = paginate(scope)
|
|
16
16
|
@categories = Document.distinct.order(:category).pluck(:category)
|
|
17
17
|
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,
|
|
@@ -8,13 +8,15 @@ module HrLite
|
|
|
8
8
|
def index
|
|
9
9
|
@status = params[:status].presence_in(Expense::STATUSES) || "submitted"
|
|
10
10
|
scope = Expense.where(status: @status).includes(:user, :category).recent_first
|
|
11
|
-
@expenses = paginate(hr_scope(scope, "expense.approve"))
|
|
11
|
+
@expenses = paginate(hr_scope(scope, "expense.approve").where.not(user_id: hr_current_user.id))
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def approve
|
|
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
|
|
@@ -35,6 +39,18 @@ module HrLite
|
|
|
35
39
|
redirect_to admin_expenses_path, alert: "Only an approved claim can be reimbursed."
|
|
36
40
|
end
|
|
37
41
|
|
|
42
|
+
# The receipt the employee had to upload, for the person deciding; same rules as a document download.
|
|
43
|
+
def receipt
|
|
44
|
+
expense = decidable.find(params[:id])
|
|
45
|
+
raise ActiveRecord::RecordNotFound unless expense.receipt.attached?
|
|
46
|
+
|
|
47
|
+
AuditLog.record!(action: "expense.receipt_viewed", subject: expense, actor: hr_current_user,
|
|
48
|
+
changes: { "owner" => HrLite.display_name(expense.user) })
|
|
49
|
+
redirect_to Rails.application.routes.url_helpers.rails_blob_path(
|
|
50
|
+
expense.receipt, disposition: "inline", only_path: true, expires_in: 5.minutes
|
|
51
|
+
), allow_other_host: false
|
|
52
|
+
end
|
|
53
|
+
|
|
38
54
|
private
|
|
39
55
|
|
|
40
56
|
def require_deciding! = hr_require_permission!("expense.approve", scope: :team)
|
|
@@ -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
|
|
@@ -8,7 +8,7 @@ module HrLite
|
|
|
8
8
|
# the LIST as well as the decision matters: an index that shows a row
|
|
9
9
|
# the member action then refuses is a worse screen than one that never
|
|
10
10
|
# showed it.
|
|
11
|
-
@requests = paginate(hr_scope(scope.where(status: @status), "leave.view"))
|
|
11
|
+
@requests = paginate(hr_scope(scope.where(status: @status), "leave.view").where.not(user_id: hr_current_user.id))
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def show
|
|
@@ -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),
|
|
@@ -4,7 +4,7 @@ module HrLite
|
|
|
4
4
|
def index
|
|
5
5
|
@status = params[:status].presence_in(RegularizationRequest::STATUSES) || "pending"
|
|
6
6
|
@requests = paginate(hr_scope(RegularizationRequest.includes(:user).where(status: @status).recent_first,
|
|
7
|
-
"attendance.view"))
|
|
7
|
+
"attendance.view").where.not(user_id: hr_current_user.id))
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def show
|
|
@@ -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
|