hr_lite 0.53.0 → 0.54.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 +13 -0
- data/README.md +5 -2
- data/app/assets/javascripts/hr_lite/emoji_pick.js +4 -4
- data/app/assets/stylesheets/hr_lite/hr_lite.css +7 -6
- data/app/controllers/hr_lite/admin/attendances_controller.rb +2 -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/expenses_controller.rb +13 -1
- data/app/controllers/hr_lite/admin/leave_requests_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/regularization_requests_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/statutory_rate_cards_controller.rb +2 -1
- data/app/controllers/hr_lite/admin/tax_declarations_controller.rb +2 -1
- data/app/controllers/hr_lite/admin/work_locations_controller.rb +1 -1
- data/app/controllers/hr_lite/home_controller.rb +0 -9
- data/app/controllers/hr_lite/loans_controller.rb +1 -1
- data/app/helpers/hr_lite/application_helper.rb +21 -2
- data/app/models/hr_lite/attendance_record.rb +7 -4
- data/app/models/hr_lite/salary_slip.rb +2 -1
- data/app/models/hr_lite/statutory_rate_card_record.rb +6 -0
- data/app/views/hr_lite/admin/attendances/index.html.erb +4 -4
- data/app/views/hr_lite/admin/attendances/show.html.erb +1 -1
- data/app/views/hr_lite/admin/benefits/index.html.erb +10 -4
- data/app/views/hr_lite/admin/comp_off_requests/show.html.erb +1 -1
- data/app/views/hr_lite/admin/documents/index.html.erb +2 -2
- data/app/views/hr_lite/admin/employees/show.html.erb +2 -1
- data/app/views/hr_lite/admin/expense_categories/index.html.erb +1 -1
- data/app/views/hr_lite/admin/expenses/index.html.erb +4 -2
- data/app/views/hr_lite/admin/holidays/index.html.erb +2 -1
- data/app/views/hr_lite/admin/hr_requests/index.html.erb +2 -2
- data/app/views/hr_lite/admin/hr_requests/show.html.erb +1 -1
- data/app/views/hr_lite/admin/leave_requests/show.html.erb +1 -1
- data/app/views/hr_lite/admin/leave_types/index.html.erb +2 -2
- data/app/views/hr_lite/admin/loans/new.html.erb +1 -1
- 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 +1 -1
- data/app/views/hr_lite/admin/payroll_runs/show.html.erb +6 -4
- data/app/views/hr_lite/admin/performance_plans/_form.html.erb +2 -1
- data/app/views/hr_lite/admin/policies/index.html.erb +1 -1
- data/app/views/hr_lite/admin/policies/new.html.erb +2 -1
- data/app/views/hr_lite/admin/regularization_requests/show.html.erb +1 -1
- data/app/views/hr_lite/admin/roles/_form.html.erb +2 -1
- 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/statutory_rate_cards/_form.html.erb +11 -7
- data/app/views/hr_lite/admin/tax_declarations/show.html.erb +6 -4
- data/app/views/hr_lite/admin/wfh_requests/show.html.erb +1 -1
- data/app/views/hr_lite/approvals/index.html.erb +4 -3
- data/app/views/hr_lite/attendance/_custom_status.html.erb +7 -9
- data/app/views/hr_lite/attendance/_punch_card.html.erb +8 -0
- data/app/views/hr_lite/attendance/show.html.erb +5 -3
- data/app/views/hr_lite/home/index.html.erb +4 -3
- data/app/views/hr_lite/leave_requests/_balance_chips.html.erb +2 -1
- data/app/views/hr_lite/loans/index.html.erb +1 -1
- data/app/views/hr_lite/resignations/show.html.erb +2 -1
- data/app/views/hr_lite/salary_slips/_slip_detail.html.erb +5 -5
- data/app/views/hr_lite/salary_slips/pdf.html.erb +1 -0
- data/app/views/hr_lite/work_time/_bars.html.erb +4 -3
- 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/statutory_seeds.rb +1 -1
- data/lib/hr_lite/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2914dd05ed8eb6c4e9974e4f4d84ee8149f6158e976ee6f7ec07958b8046a894
|
|
4
|
+
data.tar.gz: 36bc28076ab2d1d16a6ac20df8171c28ae50b317081ec3e2a6b205cf32591d05
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1235ee079d20011f1d966c605814f028ece64a6d148c6ad8e28be4b1a9db7cb1b9ac98ded5b1f7c87a6c10536c7d524788b0f87096be4146f2fbe2458b08d0c4
|
|
7
|
+
data.tar.gz: a770905bf3daeab581c3edc9a3fb7d4902824d3c47a491e5af540123df3c6aaad711d9d6de88b6a0055acc9432750dd9802ce3c0426c74b1070293d81e24a835
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.54.0] - 2026-10-17
|
|
11
|
+
|
|
12
|
+
Fixes from the HRMS UI/UX audit of 0.52.0. No migrations.
|
|
13
|
+
|
|
14
|
+
- 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.
|
|
15
|
+
- 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).
|
|
16
|
+
- 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.
|
|
17
|
+
- 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.
|
|
18
|
+
- 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".
|
|
19
|
+
- 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".
|
|
20
|
+
- 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.
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
10
23
|
## [0.53.0] - 2026-10-16
|
|
11
24
|
|
|
12
25
|
- 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.
|
|
@@ -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
|
})();
|
|
@@ -124,7 +124,7 @@ html:has(.hrl-body) { scroll-padding-top: 4rem; scroll-padding-bottom: calc(4.4r
|
|
|
124
124
|
.hrl-nav__link--active .hrl-nav__hint { color: inherit; }
|
|
125
125
|
/* Tab row: the current place's tabs on one line; it scrolls sideways, never the page. */
|
|
126
126
|
.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; }
|
|
127
|
+
.hrl-tabrow .hrl-nav__link { flex: 0 0 auto; white-space: nowrap; min-width: var(--hrl-target); justify-content: center; }
|
|
128
128
|
.hrl-side__divider { height: 1px; background: var(--hrl-line); margin: var(--hrl-s-2) var(--hrl-s-1); }
|
|
129
129
|
|
|
130
130
|
/* Mobile app bar */
|
|
@@ -415,12 +415,14 @@ fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
|
|
|
415
415
|
.hrl-card--private .hrl-badge--muted { background: var(--hrl-card); }
|
|
416
416
|
|
|
417
417
|
/* Tax: the two regimes side by side, then the working as a plain table. */
|
|
418
|
+
.hrl-slabs { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--hrl-gap-target); align-items: end; }
|
|
418
419
|
.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
420
|
.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
421
|
.hrl-tax-compare__col--chosen { border: 2px solid var(--hrl-accent); }
|
|
421
422
|
.hrl-tax-compare__amount { font-size: var(--hrl-t-title); font-weight: 700; margin-top: var(--hrl-s-1); overflow-wrap: anywhere; }
|
|
422
423
|
.hrl-tax-working { margin-top: var(--hrl-s-3); }
|
|
423
|
-
.hrl-tax-working__table td { vertical-align: top; overflow-wrap:
|
|
424
|
+
.hrl-tax-working__table td { vertical-align: top; overflow-wrap: break-word; } /* anywhere let the table squeeze amounts mid-number */
|
|
425
|
+
.hrl-tax-working__table td.hrl-num { white-space: nowrap; }
|
|
424
426
|
.hrl-tax-working__total td { font-weight: 700; border-top: 1px solid var(--hrl-line); }
|
|
425
427
|
.hrl-tax-cap { display: block; width: 100%; height: 8px; margin-top: var(--hrl-s-1); accent-color: var(--hrl-accent); }
|
|
426
428
|
|
|
@@ -621,8 +623,9 @@ a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--
|
|
|
621
623
|
.hrl-donut__track { stroke: var(--hrl-bg); }
|
|
622
624
|
.hrl-swatch { display: inline-block; width: .7rem; height: .7rem; border-radius: var(--hrl-radius-pill); vertical-align: middle; }
|
|
623
625
|
.hrl-chart-scroll { overflow-x: auto; }
|
|
624
|
-
|
|
625
|
-
.hrl-
|
|
626
|
+
/* 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.4rem)); height: 10rem; }
|
|
628
|
+
.hrl-bars__label { font-size: 9px; fill: var(--hrl-muted); }
|
|
626
629
|
.hrl-daystrip { width: 100%; height: 2rem; background: var(--hrl-bg); border-radius: var(--hrl-radius-sm); }
|
|
627
630
|
.hrl-timeline__auto { stroke: var(--hrl-card); stroke-width: .3; stroke-dasharray: .6 .4; }
|
|
628
631
|
.hrl-timeline__axis { display: flex; justify-content: space-between; }
|
|
@@ -630,8 +633,6 @@ a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--
|
|
|
630
633
|
.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
634
|
[data-hrl-tip]:focus { outline: 2px solid var(--hrl-accent); outline-offset: 1px; }
|
|
632
635
|
.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
636
|
/* Today's check-ins: one initials badge per person; hover/tap shows times. */
|
|
636
637
|
.hrl-checkins + .hrl-checkins { margin-top: .6rem; }
|
|
637
638
|
.hrl-checkins__people { display: flex; flex-wrap: wrap; gap: var(--hrl-gap-target); margin: .4rem 0 0; padding: 0; list-style: none; }
|
|
@@ -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
|
|
@@ -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
|
|
@@ -8,7 +8,7 @@ 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
|
|
@@ -35,6 +35,18 @@ module HrLite
|
|
|
35
35
|
redirect_to admin_expenses_path, alert: "Only an approved claim can be reimbursed."
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# The receipt the employee had to upload, for the person deciding; same rules as a document download.
|
|
39
|
+
def receipt
|
|
40
|
+
expense = decidable.find(params[:id])
|
|
41
|
+
raise ActiveRecord::RecordNotFound unless expense.receipt.attached?
|
|
42
|
+
|
|
43
|
+
AuditLog.record!(action: "expense.receipt_viewed", subject: expense, actor: hr_current_user,
|
|
44
|
+
changes: { "owner" => HrLite.display_name(expense.user) })
|
|
45
|
+
redirect_to Rails.application.routes.url_helpers.rails_blob_path(
|
|
46
|
+
expense.receipt, disposition: "inline", only_path: true, expires_in: 5.minutes
|
|
47
|
+
), allow_other_host: false
|
|
48
|
+
end
|
|
49
|
+
|
|
38
50
|
private
|
|
39
51
|
|
|
40
52
|
def require_deciding! = hr_require_permission!("expense.approve", scope: :team)
|
|
@@ -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
|
|
@@ -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
|
|
@@ -52,7 +52,8 @@ module HrLite
|
|
|
52
52
|
def next_financial_year(newest)
|
|
53
53
|
return FinancialYear.start_for(Date.current) if newest.nil?
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
# A mid-year card (1 Oct) must not push the next one to 1 Oct: years start on 1 April.
|
|
56
|
+
[ FinancialYear.start_for(newest.effective_from.next_year), FinancialYear.start_for(Date.current) ].max
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
# The figures arrive as flat strings from number fields and are stored
|
|
@@ -11,7 +11,7 @@ module HrLite
|
|
|
11
11
|
@status = params[:status].presence_in(TaxDeclaration::STATUSES) || "submitted"
|
|
12
12
|
@declarations = paginate(
|
|
13
13
|
hr_scope(TaxDeclaration.includes(:user, :tax_declaration_items), "tax.view")
|
|
14
|
-
.where(status: @status).order(created_at: :desc)
|
|
14
|
+
.where(status: @status).where.not(user_id: hr_current_user.id).order(created_at: :desc)
|
|
15
15
|
)
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -69,6 +69,7 @@ module HrLite
|
|
|
69
69
|
# Recording what the proof supported is a separate act from deciding
|
|
70
70
|
# — an amount can be corrected while the decision is still pending.
|
|
71
71
|
if @declaration.update(verification_params)
|
|
72
|
+
return verify if params[:verify] # "Record and verify": the typed amounts are saved first
|
|
72
73
|
redirect_to admin_tax_declaration_path(@declaration), notice: "Amounts recorded."
|
|
73
74
|
else
|
|
74
75
|
@tax = TaxComputation.new(user: @declaration.user, declaration: @declaration)
|
|
@@ -5,7 +5,7 @@ module HrLite
|
|
|
5
5
|
# Coordinates are only shown here, inside attendance.view reach.
|
|
6
6
|
class WorkLocationsController < BaseController
|
|
7
7
|
def index
|
|
8
|
-
@status = params[:status].presence_in(WorkLocation::STATUSES) || "
|
|
8
|
+
@status = params[:status].presence_in(WorkLocation::STATUSES) || "pending"
|
|
9
9
|
@locations = paginate(hr_scope(WorkLocation.where(status: @status), "attendance.view")
|
|
10
10
|
.includes(:user).order(created_at: :desc))
|
|
11
11
|
end
|
|
@@ -8,7 +8,6 @@ module HrLite
|
|
|
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
|
-
@queue = queue_count
|
|
12
11
|
@mine_pending = [ LeaveRequest.includes(:leave_type), RegularizationRequest, CompOffRequest ]
|
|
13
12
|
.flat_map { |scope| scope.pending.where(user_id: hr_current_user.id).order(created_at: :desc).limit(5) }
|
|
14
13
|
.max_by(5, &:created_at)
|
|
@@ -16,14 +15,6 @@ module HrLite
|
|
|
16
15
|
|
|
17
16
|
private
|
|
18
17
|
|
|
19
|
-
# Requests with no routed flow are decided straight off the HR queues, scoped as those queues decide them.
|
|
20
|
-
def queue_count
|
|
21
|
-
leave = ApprovalFlow.for(LeaveRequest.name) ? 0 : decidable(LeaveRequest, "leave.approve")
|
|
22
|
-
leave + decidable(CompOffRequest, "leave.approve") + decidable(RegularizationRequest, "attendance.manage")
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def decidable(model, key) = hr_scope(model.pending, key).where.not(user_id: hr_current_user.id).count
|
|
26
|
-
|
|
27
18
|
# Last week's days that still need a fix (absent, no check-out, flagged, short), minus those with a pending ticket.
|
|
28
19
|
def attention_days
|
|
29
20
|
range = (Date.current - 7)..(Date.current - 1)
|
|
@@ -4,7 +4,7 @@ module HrLite
|
|
|
4
4
|
class LoansController < ApplicationController
|
|
5
5
|
def index
|
|
6
6
|
@loans = Loan.where(user_id: hr_current_user.id).order(starts_on: :desc)
|
|
7
|
-
@outstanding = @loans.sum(BigDecimal(0), &:outstanding)
|
|
7
|
+
@outstanding = @loans.active.sum(BigDecimal(0), &:outstanding)
|
|
8
8
|
@next_month = Date.current.next_month.beginning_of_month
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -44,7 +44,7 @@ module HrLite
|
|
|
44
44
|
{ label: "Attendance fixes", path: :admin_regularization_requests_path, match: [ "/admin/regularization_requests" ], if: OPS,
|
|
45
45
|
queue: -> { hrl_queue(HrLite::RegularizationRequest.pending, "attendance.manage") } },
|
|
46
46
|
{ label: "WFH", path: :admin_wfh_requests_path, match: [ "/admin/wfh_requests" ], if: OPS,
|
|
47
|
-
queue: -> { hrl_queue(HrLite::WfhRequest.pending, "attendance.manage") } },
|
|
47
|
+
queue: -> { hrl_queue(HrLite::WfhRequest.pending.overlapping(Date.current, Date.current + HrLite::WfhRequest::MAX_DAYS_AHEAD), "attendance.manage") } },
|
|
48
48
|
{ label: "Home locations", path: :admin_work_locations_path, match: [ "/admin/work_locations" ], if: OPS,
|
|
49
49
|
queue: -> { hrl_queue(HrLite::WorkLocation.pending, "attendance.manage") } },
|
|
50
50
|
{ label: "Claims", path: :admin_expenses_path, match: [ "/admin/expenses" ], if: -> { hr_can?("expense.approve", scope: :team) },
|
|
@@ -91,7 +91,8 @@ module HrLite
|
|
|
91
91
|
{ label: "Manage policies", path: :admin_policies_path, match: [ "/admin/policies" ], if: -> { hr_can?("policy.manage", scope: :all) } }
|
|
92
92
|
] },
|
|
93
93
|
{ label: "Admin", divider: true, tabs: [
|
|
94
|
-
{ label: "Settings", path: :admin_leave_types_path, match: [ "/admin/leave_types", "/admin/office_locations", "/admin/holidays", "/admin/
|
|
94
|
+
{ label: "Settings", path: :admin_leave_types_path, match: [ "/admin/leave_types", "/admin/office_locations", "/admin/holidays", "/admin/expense_categories" ], if: LEAD },
|
|
95
|
+
{ label: "HR settings", path: :edit_admin_setting_path, match: [ "/admin/setting" ], if: LEAD },
|
|
95
96
|
{ label: "Roles", path: :admin_roles_path, match: [ "/admin/roles" ], if: -> { hr_can?("role.manage", scope: :all) } },
|
|
96
97
|
{ label: "Statutory rates", path: :admin_statutory_rate_cards_path, match: [ "/admin/statutory_rate_cards" ], if: SUPER },
|
|
97
98
|
{ label: "Reports", path: :admin_reports_path, match: [ "/admin/reports" ], if: -> { hr_can?("expense.approve", scope: :all) || %w[leave.view attendance.view profile.view].any? { |key| hr_can?(key, scope: :team) } } },
|
|
@@ -151,6 +152,24 @@ module HrLite
|
|
|
151
152
|
# What a queue page lets this viewer decide: their reach, never their own requests.
|
|
152
153
|
def hrl_queue(relation, key) = hr_access.scope_relation(relation, key).where.not(user_id: hr_current_user.id)
|
|
153
154
|
|
|
155
|
+
# The Inbox badge's number; Home and the overview board show the same one.
|
|
156
|
+
def hrl_inbox_count
|
|
157
|
+
inbox = hrl_sections.find { |s| s[:label] == "Inbox" }
|
|
158
|
+
inbox ? hrl_waiting(inbox).to_i : 0
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# One queued request in words, and where it is decided.
|
|
162
|
+
def hrl_inbox_item(tab, record)
|
|
163
|
+
case record
|
|
164
|
+
when HrLite::LeaveRequest
|
|
165
|
+
[ "#{record.leave_type.name}, #{record.date_range_label} · #{pluralize(hrl_days(record.days_count), 'day')}", hr_lite.admin_leave_request_path(record) ]
|
|
166
|
+
when HrLite::CompOffRequest then [ "Comp-off for #{record.date_worked.strftime('%a %-d %b')}", hr_lite.admin_comp_off_request_path(record) ]
|
|
167
|
+
when HrLite::RegularizationRequest then [ "Attendance fix, #{record.date.strftime('%a %-d %b')}", hr_lite.admin_regularization_request_path(record) ]
|
|
168
|
+
when HrLite::WfhRequest then [ "WFH, #{record.dates_label}", hr_lite.admin_wfh_request_path(record) ]
|
|
169
|
+
else [ tab[:label], hr_lite.public_send(tab[:path]) ]
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
154
173
|
# Everything waiting in this viewer's queues, oldest first, as [tab, record].
|
|
155
174
|
def hrl_inbox_rows(limit = 50)
|
|
156
175
|
inbox = hrl_sections.find { |s| s[:label] == "Inbox" } or return []
|
|
@@ -57,9 +57,11 @@ module HrLite
|
|
|
57
57
|
EmployeeProfile.day_minutes_for(user_id, on: date) unless HrLite.config.work_hours.nil? || date.saturday?
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
# Hours worked, not the clock: an early start counts.
|
|
60
|
+
# Hours worked, not the clock: an early start counts. The day's penalty
|
|
61
|
+
# minutes count too, the same rule as required_check_out_at, so the card
|
|
62
|
+
# never says "day done" above "short by 20m".
|
|
61
63
|
def short?
|
|
62
|
-
worked_under?(required_minutes)
|
|
64
|
+
worked_under?(required_minutes && required_minutes + penalty_minutes)
|
|
63
65
|
end
|
|
64
66
|
|
|
65
67
|
def below_half_day?
|
|
@@ -70,10 +72,11 @@ module HrLite
|
|
|
70
72
|
def required_check_out_at
|
|
71
73
|
return unless HrLite.config.attendance_slots && check_in_at
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
check_in_at + (EmployeeProfile.day_minutes_for(user_id, on: date) + extra).minutes
|
|
75
|
+
check_in_at + (EmployeeProfile.day_minutes_for(user_id, on: date) + penalty_minutes).minutes
|
|
75
76
|
end
|
|
76
77
|
|
|
78
|
+
def penalty_minutes = AttendancePenalty.active.where(user_id: user_id, date: date).sum(:minutes)
|
|
79
|
+
|
|
77
80
|
def worked_under?(minutes) = !!(minutes && worked_duration && worked_duration < minutes.minutes)
|
|
78
81
|
|
|
79
82
|
# Check in again after checking out: once a day, inside the hours HR sets in
|
|
@@ -61,8 +61,9 @@ module HrLite
|
|
|
61
61
|
row ? Money.d(row["amount"]) : BigDecimal(0)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
# Bump the version when the PDF template changes, or the old layout serves until the cache expires.
|
|
64
65
|
def pdf_cache_key
|
|
65
|
-
[ "
|
|
66
|
+
[ "hr_lite_slip_v2", id, updated_at.to_i, payroll_run.status,
|
|
66
67
|
user_profile&.updated_at.to_i ].join("/")
|
|
67
68
|
end
|
|
68
69
|
|
|
@@ -7,6 +7,9 @@ module HrLite
|
|
|
7
7
|
|
|
8
8
|
self.table_name = "hr_lite_statutory_rate_cards"
|
|
9
9
|
|
|
10
|
+
# Shipped cards count as verified so payroll can auto-approve, but no person checked them.
|
|
11
|
+
SHIPPED_SIGN_OFF = "Shipped defaults, not checked by a person".freeze
|
|
12
|
+
|
|
10
13
|
validates :effective_from, presence: true, uniqueness: true
|
|
11
14
|
validate :effective_from_opens_a_financial_year
|
|
12
15
|
validate :carries_every_figure_payroll_reads
|
|
@@ -22,6 +25,9 @@ module HrLite
|
|
|
22
25
|
|
|
23
26
|
def verified? = verified_by.present? && verified_on.present?
|
|
24
27
|
|
|
28
|
+
# Cards seeded before this read "hr_lite 0.52.0 (official source …)": a version is not a sign-off.
|
|
29
|
+
def verified_by = super&.start_with?("hr_lite ") ? SHIPPED_SIGN_OFF : super
|
|
30
|
+
|
|
25
31
|
# The shape SlipBuilder and the calculators expect: symbol keys and
|
|
26
32
|
# BigDecimal everywhere, because JSON gives back strings and floats and
|
|
27
33
|
# a float has no business in a PF calculation.
|
|
@@ -15,10 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
<div class="hrl-kpis">
|
|
17
17
|
<div class="hrl-kpi"><div class="hrl-kpi__value"><%= @records.values.count { |r| r.check_in_at } %></div><div class="hrl-kpi__label">Checked in</div></div>
|
|
18
|
-
<%#
|
|
19
|
-
|
|
20
|
-
quietly stopped adding up to the headcount. %>
|
|
21
|
-
<div class="hrl-kpi"><div class="hrl-kpi__value"><%= @employees.size - @records.values.count { |r| r.check_in_at } %></div><div class="hrl-kpi__label">No punch</div></div>
|
|
18
|
+
<%# Expected in and not checked in: people on leave, a holiday or a weekly off are not counted. %>
|
|
19
|
+
<div class="hrl-kpi"><div class="hrl-kpi__value"><%= @employees.count { |u| @days[u.id]&.kind == :absent } %></div><div class="hrl-kpi__label">No punch</div></div>
|
|
22
20
|
<div class="hrl-kpi"><div class="hrl-kpi__value"><%= @flagged_count %></div><div class="hrl-kpi__label">Flagged</div></div>
|
|
23
21
|
<div class="hrl-kpi"><div class="hrl-kpi__value"><%= @records.values.count { |r| r.check_in_at && r.check_out_at.nil? } %></div><div class="hrl-kpi__label">Still in</div></div>
|
|
24
22
|
</div>
|
|
@@ -71,6 +69,8 @@
|
|
|
71
69
|
<span class="hrl-badge hrl-badge--bad">Flagged</span> <span class="hrl-small"><%= record.flag_note %></span>
|
|
72
70
|
<% elsif record&.check_in_at %>
|
|
73
71
|
<span class="hrl-badge hrl-badge--ok"><%= record.status.humanize %><%= " (#{record.half_day_part} half)" if record.half_day_part %></span>
|
|
72
|
+
<% elsif @days[employee.id] %>
|
|
73
|
+
<%= hrl_team_status(@days[employee.id]) %>
|
|
74
74
|
<% else %>
|
|
75
75
|
<span class="hrl-badge hrl-badge--muted">No punch</span>
|
|
76
76
|
<% end %>
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
</div>
|
|
81
81
|
<div class="hrl-field">
|
|
82
82
|
<%= f.label :regularization_note, "Why is this being fixed? (required)" %>
|
|
83
|
-
<%= f.text_area :regularization_note %>
|
|
83
|
+
<%= f.text_area :regularization_note, required: true %>
|
|
84
84
|
</div>
|
|
85
85
|
<div class="hrl-form-actions">
|
|
86
86
|
<%= f.submit "Save fix", class: "hrl-btn hrl-btn--primary" %>
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
<%= form_with url: hr_lite.enrol_admin_benefit_path(benefit), method: :post, local: true do %>
|
|
21
21
|
<div class="hrl-field">
|
|
22
22
|
<%= label_tag "enrol_user_#{benefit.id}", "Employee" %>
|
|
23
|
-
<%= select_tag :user_id, options_for_select(people), id: "enrol_user_#{benefit.id}"
|
|
23
|
+
<%= select_tag :user_id, options_for_select(people), id: "enrol_user_#{benefit.id}", include_blank: "Choose…", required: true,
|
|
24
|
+
data: { hrl_filter: "Type a name to filter" } %>
|
|
24
25
|
</div>
|
|
25
26
|
<div class="hrl-field">
|
|
26
27
|
<%= label_tag "enrol_on_#{benefit.id}", "Covered from" %>
|
|
@@ -49,10 +50,15 @@
|
|
|
49
50
|
<td data-label="Since"><%= enrolment.enrolled_on.strftime("%d %b %Y") %></td>
|
|
50
51
|
<td data-label="Dependants" class="hrl-num"><%= enrolment.dependants %></td>
|
|
51
52
|
<td data-label="">
|
|
52
|
-
|
|
53
|
+
<% if enrolment.ended_on %>
|
|
54
|
+
<p class="hrl-small">Cover ends <%= enrolment.ended_on.strftime("%d %b %Y") %></p>
|
|
55
|
+
<% end %>
|
|
56
|
+
<%= form_with url: hr_lite.unenrol_admin_benefit_path(enrolment.benefit), method: :post, class: "hrl-row",
|
|
57
|
+
data: { turbo_confirm: "End #{enrolment.benefit.name} for #{hr_display_name(enrolment.user)} on the date picked?" } do %>
|
|
53
58
|
<%= hidden_field_tag :user_id, enrolment.user_id %>
|
|
54
|
-
<%=
|
|
55
|
-
<%=
|
|
59
|
+
<%= label_tag "ended-on-#{enrolment.id}", "Last day of cover", class: "hrl-small" %>
|
|
60
|
+
<%= date_field_tag :ended_on, enrolment.ended_on || Date.current, id: "ended-on-#{enrolment.id}", class: "hrl-input" %>
|
|
61
|
+
<%= submit_tag enrolment.ended_on ? "Change end" : "End cover", class: "hrl-btn" %>
|
|
56
62
|
<% end %>
|
|
57
63
|
</td>
|
|
58
64
|
</tr>
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
<%= form_with url: hr_lite.reject_admin_comp_off_request_path(@request), method: :post, local: true do |f| %>
|
|
52
52
|
<div class="hrl-field">
|
|
53
53
|
<%= f.label :decision_note, "Reason (required)", for: "reject_note" %>
|
|
54
|
-
<%= f.text_area :decision_note, id: "reject_note" %>
|
|
54
|
+
<%= f.text_area :decision_note, id: "reject_note", required: true %>
|
|
55
55
|
</div>
|
|
56
56
|
<div class="hrl-form-actions">
|
|
57
57
|
<%= f.button "Reject", type: :submit, class: "hrl-btn hrl-btn--danger" %>
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
</td>
|
|
36
36
|
<td data-label="File">
|
|
37
37
|
<% if doc.readable_by?(hr_current_user) && doc.file.attached? %>
|
|
38
|
-
<a class="hrl-
|
|
38
|
+
<a class="hrl-btn" href="<%= hr_lite.download_document_path(doc) %>">Open</a>
|
|
39
39
|
<% else %>
|
|
40
40
|
<span class="hrl-small hrl-muted">Not yours to open</span>
|
|
41
41
|
<% end %>
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
<%= button_to "Verify", hr_lite.verify_admin_document_path(doc),
|
|
45
45
|
method: :post, class: "hrl-btn hrl-btn--primary" %>
|
|
46
46
|
<%= form_with url: hr_lite.reject_admin_document_path(doc), method: :post, local: true do %>
|
|
47
|
-
<%= text_field_tag :note, nil, placeholder: "What is wrong with it", class: "hrl-input" %>
|
|
47
|
+
<%= text_field_tag :note, nil, placeholder: "What is wrong with it", class: "hrl-input", required: true %>
|
|
48
48
|
<%= submit_tag "Reject", class: "hrl-btn" %>
|
|
49
49
|
<% end %>
|
|
50
50
|
</td>
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
<dt>Proposed last day</dt><dd><%= @pending_resignation.proposed_last_day.strftime("%d %B %Y") %></dd>
|
|
31
31
|
<% if @pending_resignation.reason.present? %><dt>Reason</dt><dd><%= @pending_resignation.reason %></dd><% end %>
|
|
32
32
|
</dl>
|
|
33
|
-
<%= form_with url: hr_lite.accept_admin_resignation_path(@pending_resignation), method: :post, local: true
|
|
33
|
+
<%= form_with url: hr_lite.accept_admin_resignation_path(@pending_resignation), method: :post, local: true,
|
|
34
|
+
data: { turbo_confirm: "Accept #{hr_display_name(@profile.user)}'s resignation? Payroll and attendance end on the last day you set." } do %>
|
|
34
35
|
<div class="hrl-field">
|
|
35
36
|
<label for="res_last_day">Confirm last working day</label>
|
|
36
37
|
<input type="date" id="res_last_day" name="last_day" class="hrl-input"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<td data-label="Monthly cap" class="hrl-num"><%= category.uncapped? ? "No cap" : hrl_money(category.monthly_cap) %></td>
|
|
19
19
|
<td data-label="Receipt"><%= category.receipt_required ? "Required" : "Optional" %></td>
|
|
20
20
|
<td data-label="Active"><%= category.active ? "Yes" : "No" %></td>
|
|
21
|
-
<td data-label=""><a class="hrl-
|
|
21
|
+
<td data-label=""><a class="hrl-btn" href="<%= hr_lite.edit_admin_expense_category_path(category) %>">Edit</a></td>
|
|
22
22
|
</tr>
|
|
23
23
|
<% end %>
|
|
24
24
|
</tbody>
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
<div class="hrl-small hrl-muted">
|
|
27
27
|
<%= expense.description %> · <%= expense.spent_on.strftime("%d %b %Y") %>
|
|
28
28
|
</div>
|
|
29
|
+
<% if expense.receipt.attached? %><a class="hrl-card__link" href="<%= hr_lite.receipt_admin_expense_path(expense) %>">Open receipt</a><% end %>
|
|
29
30
|
</td>
|
|
30
31
|
<td data-label="Amount" class="hrl-num"><%= hrl_money(expense.amount) %></td>
|
|
31
32
|
<td data-label="">
|
|
@@ -33,8 +34,9 @@
|
|
|
33
34
|
<%= form_with url: hr_lite.approve_admin_expense_path(expense), method: :post do %>
|
|
34
35
|
<%= submit_tag "Approve", class: "hrl-btn hrl-btn--primary" %>
|
|
35
36
|
<% end %>
|
|
36
|
-
<%= form_with url: hr_lite.reject_admin_expense_path(expense), method: :post do %>
|
|
37
|
-
<%=
|
|
37
|
+
<%= form_with url: hr_lite.reject_admin_expense_path(expense), method: :post, class: "hrl-row hrl-mt" do %>
|
|
38
|
+
<%= label_tag "reject-expense-#{expense.id}", "Reason to reject", class: "hrl-sr-only" %>
|
|
39
|
+
<%= text_field_tag :decision_note, nil, id: "reject-expense-#{expense.id}", placeholder: "Reason to reject", required: true, class: "hrl-input" %>
|
|
38
40
|
<%= submit_tag "Reject", class: "hrl-btn" %>
|
|
39
41
|
<% end %>
|
|
40
42
|
<% elsif expense.approved? %>
|
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
<%= select_tag :office_location_id, options_for_select(@offices), include_blank: "All offices", id: "bulk-office", data: { hrl_filter: ("Type an office" if @offices.size > 5) } %></div>
|
|
59
59
|
<% end %>
|
|
60
60
|
<div class="hrl-field">
|
|
61
|
-
<
|
|
61
|
+
<label for="bulk-lines">One holiday a line: date, name</label>
|
|
62
|
+
<textarea name="lines" id="bulk-lines" class="hrl-input" rows="6" placeholder="2026-11-09, Diwali 2026-03-04, Holi"></textarea>
|
|
62
63
|
</div>
|
|
63
64
|
<div class="hrl-form-actions">
|
|
64
65
|
<button type="submit" class="hrl-btn hrl-btn--primary">Add all</button>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
<% @requests.each do |request| %>
|
|
22
22
|
<tr>
|
|
23
23
|
<td data-label="Request">
|
|
24
|
-
<a href="<%= hr_lite.admin_hr_request_path(request) %>"><%= request.subject %></a>
|
|
24
|
+
<a class="hrl-card__link" href="<%= hr_lite.admin_hr_request_path(request) %>"><%= request.subject %></a>
|
|
25
25
|
<div class="hrl-small hrl-muted"><%= request.category_label %></div>
|
|
26
26
|
</td>
|
|
27
27
|
<td data-label="Who"><%= hr_display_name(request.user) %></td>
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
<%= request.assigned_to ? hr_display_name(request.assigned_to) : "—" %>
|
|
30
30
|
</td>
|
|
31
31
|
<td data-label="">
|
|
32
|
-
<a class="hrl-
|
|
32
|
+
<a class="hrl-btn" href="<%= hr_lite.admin_hr_request_path(request) %>">Open</a>
|
|
33
33
|
</td>
|
|
34
34
|
</tr>
|
|
35
35
|
<% end %>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
<%= form_with url: hr_lite.resolve_admin_hr_request_path(@request), method: :post do %>
|
|
22
22
|
<div class="hrl-field">
|
|
23
23
|
<%= label_tag :resolution, "What you did" %>
|
|
24
|
-
<%= text_area_tag :resolution, nil, rows: 4 %>
|
|
24
|
+
<%= text_area_tag :resolution, nil, rows: 4, required: true %>
|
|
25
25
|
</div>
|
|
26
26
|
<div class="hrl-form-actions">
|
|
27
27
|
<%= submit_tag "Send answer", class: "hrl-btn hrl-btn--primary" %>
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
<%= form_with url: hr_lite.reject_admin_leave_request_path(@request), method: :post, local: true do %>
|
|
77
77
|
<div class="hrl-field">
|
|
78
78
|
<label for="reject_note">Reason (required)</label>
|
|
79
|
-
<textarea name="decision_note" id="reject_note" class="hrl-input"></textarea>
|
|
79
|
+
<textarea name="decision_note" id="reject_note" class="hrl-input" required></textarea>
|
|
80
80
|
</div>
|
|
81
81
|
<div class="hrl-form-actions">
|
|
82
82
|
<button class="hrl-btn hrl-btn--danger" type="submit">Reject</button>
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
<td data-label="Carry cap" class="hrl-num"><%= type.carry_forward_cap.to_f %></td>
|
|
24
24
|
<td data-label="Paid"><%= type.paid ? "Yes" : "No" %></td>
|
|
25
25
|
<td data-label="">
|
|
26
|
-
<a class="hrl-
|
|
26
|
+
<a class="hrl-btn" href="<%= hr_lite.edit_admin_leave_type_path(type) %>">Edit</a>
|
|
27
27
|
</td>
|
|
28
28
|
</tr>
|
|
29
29
|
<% end %>
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
<a class="hrl-btn" href="<%= hr_lite.admin_office_locations_path %>">Office locations</a>
|
|
37
37
|
<a class="hrl-btn" href="<%= hr_lite.admin_holidays_path %>">Holidays</a>
|
|
38
38
|
<a class="hrl-btn" href="<%= hr_lite.admin_expense_categories_path %>">Expense categories</a>
|
|
39
|
-
<a class="hrl-btn" href="<%= hr_lite.edit_admin_setting_path %>">
|
|
39
|
+
<a class="hrl-btn" href="<%= hr_lite.edit_admin_setting_path %>">HR settings</a>
|
|
40
40
|
</div>
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<%= f.label :user_id, "Employee" %>
|
|
14
14
|
<%= f.select :user_id, options_for_select(HrLite.employees.map { |u| [ hr_display_name(u), u.id ] },
|
|
15
15
|
@loan.user_id),
|
|
16
|
-
{}, data: { hrl_filter: "Type a name to filter" } %>
|
|
16
|
+
{ include_blank: "Choose…" }, required: true, data: { hrl_filter: "Type a name to filter" } %>
|
|
17
17
|
</div>
|
|
18
18
|
<div class="hrl-field">
|
|
19
19
|
<%= f.label :principal, "Amount lent (₹)" %>
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<td data-label="Coordinates"><%= office.lat %>, <%= office.lng %></td>
|
|
19
19
|
<td data-label="Radius" class="hrl-num"><%= office.radius_m %> m</td>
|
|
20
20
|
<td data-label="Active"><%= office.active ? "Yes" : "No" %></td>
|
|
21
|
-
<td data-label=""><a class="hrl-
|
|
21
|
+
<td data-label=""><a class="hrl-btn" href="<%= hr_lite.edit_admin_office_location_path(office) %>">Edit</a></td>
|
|
22
22
|
</tr>
|
|
23
23
|
<% end %>
|
|
24
24
|
</tbody>
|