hr_lite 0.16.0 → 0.18.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +63 -0
  3. data/README.md +1 -0
  4. data/app/controllers/hr_lite/admin/benefits_controller.rb +61 -0
  5. data/app/controllers/hr_lite/admin/expense_categories_controller.rb +43 -0
  6. data/app/controllers/hr_lite/admin/leave_balances_controller.rb +4 -1
  7. data/app/controllers/hr_lite/admin/leave_requests_controller.rb +3 -3
  8. data/app/controllers/hr_lite/admin/payroll_line_items_controller.rb +47 -0
  9. data/app/controllers/hr_lite/admin/payroll_runs_controller.rb +3 -1
  10. data/app/controllers/hr_lite/leave_requests_controller.rb +3 -1
  11. data/app/controllers/hr_lite/salary_slips_controller.rb +6 -6
  12. data/app/helpers/hr_lite/application_helper.rb +12 -6
  13. data/app/jobs/hr_lite/payroll_auto_draft_job.rb +4 -3
  14. data/app/jobs/hr_lite/slips_ready_job.rb +10 -0
  15. data/app/models/hr_lite/audit_log.rb +1 -1
  16. data/app/models/hr_lite/benefit_enrolment.rb +1 -1
  17. data/app/models/hr_lite/leave_balance.rb +2 -1
  18. data/app/models/hr_lite/leave_request.rb +38 -17
  19. data/app/models/hr_lite/payroll_line_item.rb +5 -3
  20. data/app/models/hr_lite/payroll_run.rb +19 -3
  21. data/app/models/hr_lite/salary_slip.rb +6 -0
  22. data/app/services/hr_lite/attendance_summary.rb +12 -15
  23. data/app/views/hr_lite/admin/benefits/index.html.erb +65 -0
  24. data/app/views/hr_lite/admin/benefits/new.html.erb +37 -0
  25. data/app/views/hr_lite/admin/expense_categories/_form.html.erb +16 -0
  26. data/app/views/hr_lite/admin/expense_categories/edit.html.erb +3 -0
  27. data/app/views/hr_lite/admin/expense_categories/index.html.erb +30 -0
  28. data/app/views/hr_lite/admin/expense_categories/new.html.erb +3 -0
  29. data/app/views/hr_lite/admin/leave_balances/index.html.erb +4 -0
  30. data/app/views/hr_lite/admin/leave_requests/show.html.erb +3 -0
  31. data/app/views/hr_lite/admin/leave_types/index.html.erb +1 -0
  32. data/app/views/hr_lite/admin/payroll_line_items/index.html.erb +47 -0
  33. data/app/views/hr_lite/admin/payroll_line_items/new.html.erb +42 -0
  34. data/app/views/hr_lite/admin/payroll_runs/show.html.erb +1 -0
  35. data/app/views/hr_lite/admin/salary_structures/_form.html.erb +5 -1
  36. data/app/views/hr_lite/admin/shared/_approvals_tabs.html.erb +3 -1
  37. data/app/views/hr_lite/leave_requests/index.html.erb +1 -0
  38. data/app/views/hr_lite/leave_requests/new.html.erb +1 -0
  39. data/app/views/hr_lite/salary_slips/index.html.erb +1 -1
  40. data/app/views/layouts/hr_lite/application.html.erb +4 -4
  41. data/config/routes.rb +5 -0
  42. data/db/migrate/20260929081109_add_paid_days_to_hr_lite_leave_requests.rb +5 -0
  43. data/lib/hr_lite/configuration.rb +11 -1
  44. data/lib/hr_lite/notifications.rb +6 -2
  45. data/lib/hr_lite/version.rb +1 -1
  46. metadata +14 -1
@@ -2,7 +2,8 @@ module HrLite
2
2
  # Month roll-up per user — the payroll contract. Exact math per date:
3
3
  #
4
4
  # holiday / weekend / present / paid full-day leave -> payable +1
5
- # unpaid full-day leave (LWP) / absent -> lop +1
5
+ # unpaid full-day leave (LWP, or a paid-type day beyond the balance —
6
+ # LeaveRequest#unpaid_on) / absent -> lop +1
6
7
  # half-day punch (no leave) -> payable +0.5, lop +0.5
7
8
  # half-day PAID leave: leave half payable; other half payable if
8
9
  # punched, else lop (unpaid half-day leave: leave half is lop too)
@@ -54,13 +55,11 @@ module HrLite
54
55
  summary[:payable_days] += BigDecimal("0.5")
55
56
  summary[:lop_days] += BigDecimal("0.5")
56
57
  when :leave
57
- if day.leave.paid?
58
- summary[:paid_leave] += 1
59
- summary[:payable_days] += 1
60
- else
61
- summary[:unpaid_leave] += 1
62
- summary[:lop_days] += 1
63
- end
58
+ unpaid = day.leave.paid? ? day.leave.unpaid_on(date) : 1
59
+ summary[:paid_leave] += 1 - unpaid
60
+ summary[:payable_days] += 1 - unpaid
61
+ summary[:unpaid_leave] += unpaid
62
+ summary[:lop_days] += unpaid
64
63
  when :half_day_leave
65
64
  apply_half_day_leave(summary, day)
66
65
  when :upcoming
@@ -77,13 +76,11 @@ module HrLite
77
76
  def self.apply_half_day_leave(summary, day)
78
77
  half = BigDecimal("0.5")
79
78
 
80
- if day.leave.paid?
81
- summary[:paid_leave] += half
82
- summary[:payable_days] += half
83
- else
84
- summary[:unpaid_leave] += half
85
- summary[:lop_days] += half
86
- end
79
+ unpaid = day.leave.paid? ? day.leave.unpaid_on(day.leave.start_date) : half
80
+ summary[:paid_leave] += half - unpaid
81
+ summary[:payable_days] += half - unpaid
82
+ summary[:unpaid_leave] += unpaid
83
+ summary[:lop_days] += unpaid
87
84
 
88
85
  if day.record&.check_in_at
89
86
  summary[:present] += half
@@ -0,0 +1,65 @@
1
+ <% content_for(:page_title) { "Benefits" } %>
2
+ <div class="hrl-page__head">
3
+ <h1 class="hrl-page__title">Benefits</h1>
4
+ <div class="hrl-page__actions">
5
+ <a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_admin_benefit_path %>">Add benefit</a>
6
+ </div>
7
+ </div>
8
+
9
+ <% if @benefits.none? %>
10
+ <section class="hrl-card"><p class="hrl-card--empty">No benefits yet — add one, then enrol people here.</p></section>
11
+ <% end %>
12
+ <% people = HrLite.active_employees.map { |u| [ hr_display_name(u), u.id ] } %>
13
+ <% @benefits.each do |benefit| %>
14
+ <section class="hrl-card">
15
+ <h2 class="hrl-card__title"><%= benefit.name %></h2>
16
+ <p class="hrl-small hrl-muted">
17
+ <%= benefit.kind.humanize %><%= " · #{benefit.provider}" if benefit.provider.present? %>
18
+ · cover <%= hrl_money(benefit.coverage) %>
19
+ </p>
20
+ <%= form_with url: hr_lite.enrol_admin_benefit_path(benefit), method: :post, local: true do %>
21
+ <div class="hrl-field">
22
+ <%= label_tag "enrol_user_#{benefit.id}", "Employee" %>
23
+ <%= select_tag :user_id, options_for_select(people), id: "enrol_user_#{benefit.id}" %>
24
+ </div>
25
+ <div class="hrl-field">
26
+ <%= label_tag "enrol_on_#{benefit.id}", "Covered from" %>
27
+ <%= date_field_tag :enrolled_on, Date.current, id: "enrol_on_#{benefit.id}" %>
28
+ </div>
29
+ <div class="hrl-field">
30
+ <%= label_tag "enrol_dependants_#{benefit.id}", "Dependants covered" %>
31
+ <%= number_field_tag :dependants, 0, min: 0, id: "enrol_dependants_#{benefit.id}" %>
32
+ </div>
33
+ <div class="hrl-form-actions"><%= submit_tag "Enrol", class: "hrl-btn hrl-btn--primary" %></div>
34
+ <% end %>
35
+ </section>
36
+ <% end %>
37
+
38
+ <% if @enrolments.any? %>
39
+ <section class="hrl-card">
40
+ <h2 class="hrl-card__title">Enrolled</h2>
41
+ <div class="hrl-table-wrap">
42
+ <table class="hrl-table hrl-table--stack">
43
+ <thead><tr><th>Who</th><th>Benefit</th><th>Since</th><th>Dependants</th><th></th></tr></thead>
44
+ <tbody>
45
+ <% @enrolments.each do |enrolment| %>
46
+ <tr>
47
+ <td data-label="Who"><%= hr_display_name(enrolment.user) %></td>
48
+ <td data-label="Benefit"><%= enrolment.benefit.name %></td>
49
+ <td data-label="Since"><%= enrolment.enrolled_on.strftime("%d %b %Y") %></td>
50
+ <td data-label="Dependants" class="hrl-num"><%= enrolment.dependants %></td>
51
+ <td data-label="">
52
+ <%= form_with url: hr_lite.unenrol_admin_benefit_path(enrolment.benefit), method: :post do %>
53
+ <%= hidden_field_tag :user_id, enrolment.user_id %>
54
+ <%= date_field_tag :ended_on, Date.current, class: "hrl-input", "aria-label": "Last day of cover" %>
55
+ <%= submit_tag "End cover", class: "hrl-btn" %>
56
+ <% end %>
57
+ </td>
58
+ </tr>
59
+ <% end %>
60
+ </tbody>
61
+ </table>
62
+ </div>
63
+ <%= hrl_pagination %>
64
+ </section>
65
+ <% end %>
@@ -0,0 +1,37 @@
1
+ <% content_for(:page_title) { "Add benefit" } %>
2
+ <div class="hrl-page__head"><h1 class="hrl-page__title">Add benefit</h1></div>
3
+
4
+ <section class="hrl-card">
5
+ <%= form_with model: @benefit, url: hr_lite.admin_benefits_path, method: :post, local: true do |f| %>
6
+ <%= render "hr_lite/shared/form_errors", record: @benefit %>
7
+ <div class="hrl-field"><%= f.label :name %><%= f.text_field :name %></div>
8
+ <div class="hrl-field">
9
+ <%= f.label :kind %>
10
+ <%= f.select :kind, HrLite::Benefit::KINDS.map { |k| [ k.humanize, k ] } %>
11
+ </div>
12
+ <div class="hrl-field"><%= f.label :provider, "Provider (optional)" %><%= f.text_field :provider %></div>
13
+ <div class="hrl-field">
14
+ <%= f.label :policy_number, "Policy number (optional)" %>
15
+ <%= f.text_field :policy_number %>
16
+ </div>
17
+ <div class="hrl-field">
18
+ <%= f.label :coverage, "Cover (₹)" %>
19
+ <%= f.text_field :coverage, value: @benefit.coverage&.to_s("F"), inputmode: "decimal" %>
20
+ </div>
21
+ <div class="hrl-field">
22
+ <%= f.label :employer_premium, "Premium the company pays (₹)" %>
23
+ <%= f.text_field :employer_premium, value: @benefit.employer_premium&.to_s("F"), inputmode: "decimal" %>
24
+ </div>
25
+ <div class="hrl-field">
26
+ <%= f.label :employee_premium, "Premium the employee pays (₹)" %>
27
+ <%= f.text_field :employee_premium, value: @benefit.employee_premium&.to_s("F"), inputmode: "decimal" %>
28
+ </div>
29
+ <div class="hrl-field"><%= f.label :effective_from, "Starts" %><%= f.date_field :effective_from %></div>
30
+ <div class="hrl-field"><%= f.label :expires_on, "Expires" %><%= f.date_field :expires_on %></div>
31
+ <div class="hrl-field"><%= f.label :notes %><%= f.text_field :notes %></div>
32
+ <div class="hrl-form-actions">
33
+ <%= f.submit "Save", class: "hrl-btn hrl-btn--primary" %>
34
+ <a class="hrl-btn" href="<%= hr_lite.admin_benefits_path %>">Cancel</a>
35
+ </div>
36
+ <% end %>
37
+ </section>
@@ -0,0 +1,16 @@
1
+ <section class="hrl-card">
2
+ <%= form_with model: expense_category, url: url, method: method, local: true do |f| %>
3
+ <%= render "hr_lite/shared/form_errors", record: expense_category %>
4
+ <div class="hrl-field"><%= f.label :name %><%= f.text_field :name %></div>
5
+ <div class="hrl-field">
6
+ <%= f.label :monthly_cap, "Monthly cap per person (₹, blank = no cap)" %>
7
+ <%= f.number_field :monthly_cap, step: 0.01, min: 0, value: expense_category.monthly_cap&.to_s("F") %>
8
+ </div>
9
+ <div class="hrl-field hrl-field--check"><%= f.check_box :receipt_required %><%= f.label :receipt_required, "Receipt required" %></div>
10
+ <div class="hrl-field hrl-field--check"><%= f.check_box :active %><%= f.label :active, "Active (offered on the claim form)" %></div>
11
+ <div class="hrl-form-actions">
12
+ <%= f.submit "Save", class: "hrl-btn hrl-btn--primary" %>
13
+ <a class="hrl-btn" href="<%= hr_lite.admin_expense_categories_path %>">Cancel</a>
14
+ </div>
15
+ <% end %>
16
+ </section>
@@ -0,0 +1,3 @@
1
+ <% content_for(:page_title) { "Edit expense category" } %>
2
+ <div class="hrl-page__head"><h1 class="hrl-page__title">Edit <%= @expense_category.name %></h1></div>
3
+ <%= render "form", expense_category: @expense_category, url: hr_lite.admin_expense_category_path(@expense_category), method: :patch %>
@@ -0,0 +1,30 @@
1
+ <% content_for(:page_title) { "Expense categories" } %>
2
+ <div class="hrl-page__head">
3
+ <h1 class="hrl-page__title">Expense categories</h1>
4
+ <div class="hrl-page__actions">
5
+ <a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_admin_expense_category_path %>">Add category</a>
6
+ </div>
7
+ </div>
8
+
9
+ <section class="hrl-card">
10
+ <% if @expense_categories.any? %>
11
+ <div class="hrl-table-wrap">
12
+ <table class="hrl-table hrl-table--stack">
13
+ <thead><tr><th>Name</th><th>Monthly cap</th><th>Receipt</th><th>Active</th><th></th></tr></thead>
14
+ <tbody>
15
+ <% @expense_categories.each do |category| %>
16
+ <tr>
17
+ <td data-label="Name"><%= category.name %></td>
18
+ <td data-label="Monthly cap" class="hrl-num"><%= category.uncapped? ? "No cap" : hrl_money(category.monthly_cap) %></td>
19
+ <td data-label="Receipt"><%= category.receipt_required ? "Required" : "Optional" %></td>
20
+ <td data-label="Active"><%= category.active ? "Yes" : "No" %></td>
21
+ <td data-label=""><a class="hrl-small" href="<%= hr_lite.edit_admin_expense_category_path(category) %>">Edit</a></td>
22
+ </tr>
23
+ <% end %>
24
+ </tbody>
25
+ </table>
26
+ </div>
27
+ <% else %>
28
+ <p class="hrl-card--empty">No categories yet — nobody can file an expense claim until one exists.</p>
29
+ <% end %>
30
+ </section>
@@ -0,0 +1,3 @@
1
+ <% content_for(:page_title) { "Add expense category" } %>
2
+ <div class="hrl-page__head"><h1 class="hrl-page__title">Add expense category</h1></div>
3
+ <%= render "form", expense_category: @expense_category, url: hr_lite.admin_expense_categories_path, method: :post %>
@@ -3,6 +3,8 @@
3
3
  <h1 class="hrl-page__title">Team leave balances <%= HrLite::LeaveYear.label(@year) %></h1>
4
4
  </div>
5
5
 
6
+ <%= render "hr_lite/admin/shared/approvals_tabs", active: :balances %>
7
+
6
8
  <section class="hrl-card">
7
9
  <div class="hrl-table-wrap">
8
10
  <table class="hrl-table">
@@ -24,6 +26,7 @@
24
26
  </div>
25
27
  </section>
26
28
 
29
+ <% if hr_can?("leave.manage", scope: :team) %>
27
30
  <section class="hrl-card">
28
31
  <h2 class="hrl-card__title">Adjust a balance</h2>
29
32
  <p class="hrl-small hrl-muted">Positive credits (comp-off), negative debits. Fully audited.</p>
@@ -56,3 +59,4 @@
56
59
  </div>
57
60
  <% end %>
58
61
  </section>
62
+ <% end %>
@@ -18,6 +18,9 @@
18
18
  <% unless @request.leave_type.unlimited? %>
19
19
  <dt>Balance</dt>
20
20
  <dd><%= @balance.available.to_f %> of <%= @balance.entitled.to_f %> available</dd>
21
+ <% if (@request.pending? || @request.approved?) && (short = @request.days_beyond_balance).positive? %>
22
+ <dt>Loss of pay</dt><dd><%= short.to_s("F") %> day(s) beyond the balance will be unpaid</dd>
23
+ <% end %>
21
24
  <% end %>
22
25
  <% if @request.decision_note.present? %><dt>Decision note</dt><dd><%= @request.decision_note %></dd><% end %>
23
26
  </dl>
@@ -35,5 +35,6 @@
35
35
  <div class="hrl-row">
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
+ <a class="hrl-btn" href="<%= hr_lite.admin_expense_categories_path %>">Expense categories</a>
38
39
  <a class="hrl-btn" href="<%= hr_lite.edit_admin_setting_path %>">Weekend policy</a>
39
40
  </div>
@@ -0,0 +1,47 @@
1
+ <% content_for(:page_title) { "One-off pay items" } %>
2
+ <div class="hrl-page__head">
3
+ <h1 class="hrl-page__title">One-off pay items</h1>
4
+ <div class="hrl-page__actions">
5
+ <a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_admin_payroll_line_item_path(month: @month.strftime("%Y-%m")) %>">Add an item</a>
6
+ </div>
7
+ </div>
8
+
9
+ <section class="hrl-card">
10
+ <div class="hrl-row">
11
+ <a class="hrl-btn" href="<%= hr_lite.admin_payroll_line_items_path(month: (@month - 1.month).strftime("%Y-%m")) %>">&larr;</a>
12
+ <strong><%= @month.strftime("%B %Y") %></strong>
13
+ <a class="hrl-btn" href="<%= hr_lite.admin_payroll_line_items_path(month: (@month + 1.month).strftime("%Y-%m")) %>">&rarr;</a>
14
+ </div>
15
+ <p class="hrl-muted hrl-small">
16
+ Payroll reads these when the month's run is computed. Add them before
17
+ computing, or recompute the run while it is still in review.
18
+ </p>
19
+ </section>
20
+
21
+ <section class="hrl-card">
22
+ <% if @items.empty? %>
23
+ <p class="hrl-muted">Nothing added for <%= @month.strftime("%B %Y") %>.</p>
24
+ <% else %>
25
+ <div class="hrl-table-wrap">
26
+ <table class="hrl-table hrl-table--stack">
27
+ <thead><tr><th>Person</th><th>Item</th><th>Amount</th><th>Note</th><th></th></tr></thead>
28
+ <tbody>
29
+ <% @items.each do |item| %>
30
+ <tr>
31
+ <td data-label="Person"><%= hr_display_name(item.user) %></td>
32
+ <td data-label="Item"><%= item.component.label %> (<%= item.component.kind %>)</td>
33
+ <td data-label="Amount" class="hrl-num"><%= hrl_money(item.amount) %></td>
34
+ <td data-label="Note"><%= item.note %></td>
35
+ <td data-label="">
36
+ <%= button_to "Remove", hr_lite.admin_payroll_line_item_path(item),
37
+ method: :delete, class: "hrl-btn hrl-btn--danger",
38
+ form: { data: { turbo_confirm: "Remove this item?" } } %>
39
+ </td>
40
+ </tr>
41
+ <% end %>
42
+ </tbody>
43
+ </table>
44
+ </div>
45
+ <% end %>
46
+ <%= hrl_pagination %>
47
+ </section>
@@ -0,0 +1,42 @@
1
+ <% content_for(:page_title) { "Add a pay item" } %>
2
+ <div class="hrl-page__head"><h1 class="hrl-page__title">Add a pay item</h1></div>
3
+
4
+ <section class="hrl-card">
5
+ <p class="hrl-muted hrl-small">
6
+ A bonus, incentive, arrears, reimbursement or one-time deduction on one
7
+ month's payslip. Payroll picks it up when that month's run is computed —
8
+ add it first, or recompute the run while it is in review. A month that
9
+ has already been paid is refused.
10
+ </p>
11
+ <%= form_with model: @item, url: hr_lite.admin_payroll_line_items_path, method: :post, local: true do |f| %>
12
+ <%= render "hr_lite/shared/form_errors", record: @item %>
13
+ <div class="hrl-field">
14
+ <%= f.label :user_id, "Employee" %>
15
+ <%= f.select :user_id, options_for_select(HrLite.employees.map { |u| [ hr_display_name(u), u.id ] },
16
+ @item.user_id) %>
17
+ </div>
18
+ <div class="hrl-field">
19
+ <%= f.label :component_id, "Item" %>
20
+ <%# Loan repayment is left out: finalize books that line against the person's loans. %>
21
+ <%= f.select :component_id,
22
+ HrLite::SalaryComponent.active.ordered.where.not(code: "loan_repayment")
23
+ .map { |c| [ "#{c.label} (#{c.kind})", c.id ] } %>
24
+ </div>
25
+ <div class="hrl-field">
26
+ <%= f.label :period_month, "Month" %>
27
+ <%= f.month_field :period_month, value: @item.period_month&.strftime("%Y-%m") %>
28
+ </div>
29
+ <div class="hrl-field">
30
+ <%= f.label :amount, "Amount (₹)" %>
31
+ <%= f.text_field :amount, value: @item.amount&.to_s("F"), inputmode: "decimal" %>
32
+ </div>
33
+ <div class="hrl-field">
34
+ <%= f.label :note, "Note (optional)" %>
35
+ <%= f.text_field :note %>
36
+ </div>
37
+ <div class="hrl-form-actions">
38
+ <%= f.submit "Add", class: "hrl-btn hrl-btn--primary" %>
39
+ <a class="hrl-btn" href="<%= hr_lite.admin_payroll_line_items_path(month: @item.period_month&.strftime("%Y-%m")) %>">Cancel</a>
40
+ </div>
41
+ <% end %>
42
+ </section>
@@ -2,6 +2,7 @@
2
2
  <div class="hrl-page__head">
3
3
  <h1 class="hrl-page__title">Payroll — <%= @run.label %></h1>
4
4
  <div class="hrl-page__actions">
5
+ <a class="hrl-btn" href="<%= hr_lite.admin_payroll_line_items_path(month: @run.period_month.strftime("%Y-%m")) %>">One-off items</a>
5
6
  <a class="hrl-btn" href="<%= hr_lite.admin_payroll_runs_path %>">All runs</a>
6
7
  </div>
7
8
  </div>
@@ -14,7 +14,11 @@
14
14
  <div class="hrl-field hrl-field--check"><%= f.check_box :esi_applicable %><%= f.label :esi_applicable, "ESI applicable (auto-exempt above gross ceiling)" %></div>
15
15
  <div class="hrl-field">
16
16
  <%= f.label :pt_state, "Professional tax state" %>
17
- <%= f.select :pt_state, HrLite::StatutoryRateCard.for(Date.current)[:pt].keys.map { |s| [ s.humanize, s ] } %>
17
+ <%# A stored rate card carries no PT (slabs live in their own table), so list those states too. %>
18
+ <% pt_states = [ HrLite::ProfessionalTaxSlab::NO_LEVY, structure.pt_state ] |
19
+ HrLite::ProfessionalTaxSlab.distinct.order(:state).pluck(:state) |
20
+ HrLite::StatutoryRateCard.for(Date.current)[:pt].keys %>
21
+ <%= f.select :pt_state, pt_states.map { |s| [ s.humanize, s ] } %>
18
22
  </div>
19
23
  <div class="hrl-field"><%= f.label :notes %><%= f.text_area :notes %></div>
20
24
  <div class="hrl-form-actions">
@@ -1,4 +1,4 @@
1
- <%# locals: active (:leaves | :comp_off | :regularizations) %>
1
+ <%# locals: active (:leaves | :comp_off | :regularizations | :balances) %>
2
2
  <div class="hrl-row" style="margin-bottom:.85rem;">
3
3
  <a class="hrl-btn <%= 'hrl-btn--primary' if active == :leaves %>"
4
4
  href="<%= hr_lite.admin_leave_requests_path %>">Leaves</a>
@@ -8,4 +8,6 @@
8
8
  <a class="hrl-btn <%= 'hrl-btn--primary' if active == :regularizations %>"
9
9
  href="<%= hr_lite.admin_regularization_requests_path %>">Regularization
10
10
  <% if (n = HrLite::RegularizationRequest.pending.count).positive? %><span class="hrl-badge hrl-badge--warn"><%= n %></span><% end %></a>
11
+ <a class="hrl-btn <%= 'hrl-btn--primary' if active == :balances %>"
12
+ href="<%= hr_lite.admin_leave_balances_path %>">Balances</a>
11
13
  </div>
@@ -2,6 +2,7 @@
2
2
  <div class="hrl-page__head">
3
3
  <h1 class="hrl-page__title">My leaves</h1>
4
4
  <div class="hrl-page__actions">
5
+ <a class="hrl-btn" href="<%= hr_lite.leave_balances_path %>">Balances</a>
5
6
  <a class="hrl-btn" href="<%= hr_lite.comp_off_requests_path %>">Comp-off</a>
6
7
  <a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_leave_request_path %>">Apply</a>
7
8
  </div>
@@ -4,6 +4,7 @@
4
4
  </div>
5
5
 
6
6
  <%= render "hr_lite/leave_requests/balance_chips" %>
7
+ <p class="hrl-small hrl-muted">Days beyond your balance are unpaid (loss of pay), cut from that month's salary.</p>
7
8
 
8
9
  <section class="hrl-card">
9
10
  <%= form_with model: @request, url: hr_lite.leave_requests_path, local: true do |f| %>
@@ -24,6 +24,6 @@
24
24
  </div>
25
25
  <%= hrl_pagination %>
26
26
  <% else %>
27
- <p class="hrl-card--empty">No published salary slips yet.</p>
27
+ <p class="hrl-card--empty">No published salary slips yet.<% if (day = HrLite.config.slip_release_day) %> Each month's slip opens on the <%= day.ordinalize %> of the next month.<% end %></p>
28
28
  <% end %>
29
29
  </section>
@@ -30,10 +30,10 @@
30
30
  <%= hrl_nav_link(item) %>
31
31
  <% end %>
32
32
 
33
- <% if hr_admin? || hr_leadership? %>
33
+ <% if hr_admin? || hr_leadership? || hr_can?("expense.approve", scope: :team) %>
34
34
  <span class="hrl-side__group">Manage</span>
35
35
  <% hrl_nav_items(HrLite::ApplicationHelper::ADMIN_NAV_ITEMS).each do |item| %>
36
- <%= hrl_nav_link(item) %>
36
+ <%= hrl_nav_link(item) if item[:if] || hr_admin? || hr_leadership? %>
37
37
  <% end %>
38
38
  <% end %>
39
39
 
@@ -100,10 +100,10 @@
100
100
  <% rest.each do |item| %>
101
101
  <%= hrl_nav_link(item) %>
102
102
  <% end %>
103
- <% if hr_admin? || hr_leadership? %>
103
+ <% if hr_admin? || hr_leadership? || hr_can?("expense.approve", scope: :team) %>
104
104
  <span class="hrl-side__group">Manage</span>
105
105
  <% hrl_nav_items(HrLite::ApplicationHelper::ADMIN_NAV_ITEMS).each do |item| %>
106
- <%= hrl_nav_link(item) %>
106
+ <%= hrl_nav_link(item) if item[:if] || hr_admin? || hr_leadership? %>
107
107
  <% end %>
108
108
  <% end %>
109
109
  <% if hr_leadership? || hr_superadmin? %>
data/config/routes.rb CHANGED
@@ -91,11 +91,15 @@ HrLite::Engine.routes.draw do
91
91
  resources :assets, only: %i[index new create] do
92
92
  member { post :assign; post :take_back }
93
93
  end
94
+ resources :benefits, only: %i[index new create] do
95
+ member { post :enrol; post :unenrol }
96
+ end
94
97
  resources :checklists, only: :index do
95
98
  member { post :complete; post :reopen }
96
99
  end
97
100
  resources :leave_types, except: :show
98
101
  resources :office_locations, except: :show
102
+ resources :expense_categories, except: %i[show destroy]
99
103
  resources :holidays, only: %i[index create update destroy] do
100
104
  collection { post :bulk_create }
101
105
  end
@@ -119,6 +123,7 @@ HrLite::Engine.routes.draw do
119
123
  get :register
120
124
  end
121
125
  end
126
+ resources :payroll_line_items, only: %i[index new create destroy]
122
127
  resources :salary_slips, only: %i[show update]
123
128
  resource :setting, only: %i[edit update]
124
129
  resources :audit_logs, only: :index
@@ -0,0 +1,5 @@
1
+ class AddPaidDaysToHrLiteLeaveRequests < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :hr_lite_leave_requests, :paid_days, :decimal, precision: 4, scale: 1
4
+ end
5
+ end
@@ -14,7 +14,7 @@ module HrLite
14
14
  # the HR shell; nil (default) shows nothing.
15
15
  :notifications
16
16
 
17
- attr_reader :leave_year_start_month
17
+ attr_reader :leave_year_start_month, :slip_release_day
18
18
 
19
19
  # Misconfiguration must fail at boot, not as production 500s on every
20
20
  # balance screen. Accepts "7" (ENV-friendly) and validates 1..12.
@@ -27,6 +27,15 @@ module HrLite
27
27
  @leave_year_start_month = month
28
28
  end
29
29
 
30
+ # nil = an employee sees a slip the moment it is published; 10 = the
31
+ # October slip opens on 10 November. 1..28 so every month has that day.
32
+ def slip_release_day=(value)
33
+ day = value.presence && Integer(value)
34
+ raise ArgumentError, "slip_release_day must be 1..28 or nil, got #{value.inspect}" if day && !(1..28).cover?(day)
35
+
36
+ @slip_release_day = day
37
+ end
38
+
30
39
  # 0.1.0 pre-release name for public_url_base; kept as an alias so early
31
40
  # adopters' initializers don't break.
32
41
  alias_method :mail_link_base, :public_url_base
@@ -67,6 +76,7 @@ module HrLite
67
76
  @notification_matrix = nil # resolved lazily to Notifications::DEFAULT_MATRIX
68
77
  @back_link = nil # optional {label:, url:} for the shell nav
69
78
  @leave_year_start_month = 1 # 1 = calendar year; 7 = July–June leave year
79
+ @slip_release_day = nil # e.g. 10 = slips open on the 10th of the next month
70
80
 
71
81
  # Leadership onboarding/offboarding. onboard_user must return a saved
72
82
  # user record (default: create on user_class with whatever of
@@ -48,7 +48,7 @@ module HrLite
48
48
  "resignation.accepted" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
49
49
  "resignation.withdrawn" => { bell: true, email: false, leadership_email: true, leadership_bell: false },
50
50
  "employee.onboarded" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
51
- "payroll.draft_ready" => { bell: false, email: false, leadership_email: true, leadership_bell: true },
51
+ "payroll.draft_ready" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
52
52
  "leave.team_notice" => { bell: true, email: false, leadership_email: false, leadership_bell: false },
53
53
  "comp_off.requested" => { bell: true, email: false, leadership_email: true, leadership_bell: true },
54
54
  "comp_off.approved" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
@@ -73,8 +73,10 @@ module HrLite
73
73
  # employee-scoped path ("Your appraisal has been shared", /appraisals/7):
74
74
  # fanned out verbatim, leadership read them as being about themselves and
75
75
  # followed a link their own scope 404s or resolves to their own record.
76
+ # `skip_leadership:` is for a delayed employee half of an event whose
77
+ # leadership copy already went out.
76
78
  def publish(event, title:, body: nil, path: nil, bell_to: [], email_to: [], lines: [],
77
- diff: nil, link_url: nil, leadership: {})
79
+ diff: nil, link_url: nil, leadership: {}, skip_leadership: false)
78
80
  row = matrix[event.to_s]
79
81
  unless row
80
82
  Rails.logger.warn("[hr_lite] unknown notification event #{event}")
@@ -87,6 +89,8 @@ module HrLite
87
89
 
88
90
  deliver_bells(event, row, bell_to, title, body, path)
89
91
  deliver_emails(row, email_to, title, body, path, lines, link_url)
92
+ return if skip_leadership
93
+
90
94
  deliver_leadership_email(event, row, lead_title, lead_body, lead_path, lines, diff)
91
95
  deliver_leadership_bells(event, row, bell_to, lead_title, lead_body, lead_path)
92
96
  nil
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.16.0"
2
+ VERSION = "0.18.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hr_lite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kshitiz sinha
@@ -75,11 +75,13 @@ files:
75
75
  - app/controllers/hr_lite/admin/attendances_controller.rb
76
76
  - app/controllers/hr_lite/admin/audit_logs_controller.rb
77
77
  - app/controllers/hr_lite/admin/base_controller.rb
78
+ - app/controllers/hr_lite/admin/benefits_controller.rb
78
79
  - app/controllers/hr_lite/admin/checklists_controller.rb
79
80
  - app/controllers/hr_lite/admin/comp_off_requests_controller.rb
80
81
  - app/controllers/hr_lite/admin/designation_changes_controller.rb
81
82
  - app/controllers/hr_lite/admin/documents_controller.rb
82
83
  - app/controllers/hr_lite/admin/employees_controller.rb
84
+ - app/controllers/hr_lite/admin/expense_categories_controller.rb
83
85
  - app/controllers/hr_lite/admin/expenses_controller.rb
84
86
  - app/controllers/hr_lite/admin/holidays_controller.rb
85
87
  - app/controllers/hr_lite/admin/hr_requests_controller.rb
@@ -90,6 +92,7 @@ files:
90
92
  - app/controllers/hr_lite/admin/loans_controller.rb
91
93
  - app/controllers/hr_lite/admin/office_locations_controller.rb
92
94
  - app/controllers/hr_lite/admin/overview_controller.rb
95
+ - app/controllers/hr_lite/admin/payroll_line_items_controller.rb
93
96
  - app/controllers/hr_lite/admin/payroll_runs_controller.rb
94
97
  - app/controllers/hr_lite/admin/policies_controller.rb
95
98
  - app/controllers/hr_lite/admin/regularization_requests_controller.rb
@@ -137,6 +140,7 @@ files:
137
140
  - app/jobs/hr_lite/document_expiry_job.rb
138
141
  - app/jobs/hr_lite/leave_year_rollover_job.rb
139
142
  - app/jobs/hr_lite/payroll_auto_draft_job.rb
143
+ - app/jobs/hr_lite/slips_ready_job.rb
140
144
  - app/mailers/hr_lite/application_mailer.rb
141
145
  - app/mailers/hr_lite/event_mailer.rb
142
146
  - app/models/concerns/hr_lite/approvable.rb
@@ -214,6 +218,8 @@ files:
214
218
  - app/views/hr_lite/admin/attendances/index.html.erb
215
219
  - app/views/hr_lite/admin/attendances/show.html.erb
216
220
  - app/views/hr_lite/admin/audit_logs/index.html.erb
221
+ - app/views/hr_lite/admin/benefits/index.html.erb
222
+ - app/views/hr_lite/admin/benefits/new.html.erb
217
223
  - app/views/hr_lite/admin/checklists/index.html.erb
218
224
  - app/views/hr_lite/admin/comp_off_requests/index.html.erb
219
225
  - app/views/hr_lite/admin/comp_off_requests/show.html.erb
@@ -225,6 +231,10 @@ files:
225
231
  - app/views/hr_lite/admin/employees/index.html.erb
226
232
  - app/views/hr_lite/admin/employees/new.html.erb
227
233
  - app/views/hr_lite/admin/employees/show.html.erb
234
+ - app/views/hr_lite/admin/expense_categories/_form.html.erb
235
+ - app/views/hr_lite/admin/expense_categories/edit.html.erb
236
+ - app/views/hr_lite/admin/expense_categories/index.html.erb
237
+ - app/views/hr_lite/admin/expense_categories/new.html.erb
228
238
  - app/views/hr_lite/admin/expenses/index.html.erb
229
239
  - app/views/hr_lite/admin/holidays/index.html.erb
230
240
  - app/views/hr_lite/admin/hr_requests/index.html.erb
@@ -245,6 +255,8 @@ files:
245
255
  - app/views/hr_lite/admin/office_locations/index.html.erb
246
256
  - app/views/hr_lite/admin/office_locations/new.html.erb
247
257
  - app/views/hr_lite/admin/overview/index.html.erb
258
+ - app/views/hr_lite/admin/payroll_line_items/index.html.erb
259
+ - app/views/hr_lite/admin/payroll_line_items/new.html.erb
248
260
  - app/views/hr_lite/admin/payroll_runs/index.html.erb
249
261
  - app/views/hr_lite/admin/payroll_runs/new.html.erb
250
262
  - app/views/hr_lite/admin/payroll_runs/show.html.erb
@@ -361,6 +373,7 @@ files:
361
373
  - db/migrate/20260818010401_create_hr_lite_assets_and_onboarding.rb
362
374
  - db/migrate/20260927100145_add_id_card_fields_to_hr_lite_employee_profiles.rb
363
375
  - db/migrate/20260927101031_add_created_by_id_to_hr_lite_leave_requests.rb
376
+ - db/migrate/20260929081109_add_paid_days_to_hr_lite_leave_requests.rb
364
377
  - lib/generators/hr_lite/install/install_generator.rb
365
378
  - lib/generators/hr_lite/install/templates/AFTER_INSTALL
366
379
  - lib/generators/hr_lite/install/templates/initializer.rb