hr_lite 0.21.3 → 0.22.1

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +75 -0
  3. data/README.md +54 -2
  4. data/app/assets/javascripts/hr_lite/confirm.js +18 -3
  5. data/app/assets/javascripts/hr_lite/geo_punch.js +65 -10
  6. data/app/assets/javascripts/hr_lite/nav.js +17 -0
  7. data/app/assets/javascripts/hr_lite/select_filter.js +46 -0
  8. data/app/assets/stylesheets/hr_lite/hr_lite.css +219 -79
  9. data/app/controllers/hr_lite/admin/assets_controller.rb +1 -1
  10. data/app/controllers/hr_lite/admin/attendances_controller.rb +31 -0
  11. data/app/controllers/hr_lite/approvals_controller.rb +1 -12
  12. data/app/controllers/hr_lite/attendance_controller.rb +7 -0
  13. data/app/controllers/hr_lite/holidays_controller.rb +1 -0
  14. data/app/controllers/hr_lite/home_controller.rb +40 -3
  15. data/app/controllers/hr_lite/hr_requests_controller.rb +1 -1
  16. data/app/controllers/hr_lite/leave_requests_controller.rb +13 -2
  17. data/app/controllers/hr_lite/regularization_requests_controller.rb +10 -2
  18. data/app/controllers/hr_lite/tax_declarations_controller.rb +4 -0
  19. data/app/helpers/hr_lite/application_helper.rb +43 -30
  20. data/app/jobs/hr_lite/attendance_close_job.rb +6 -1
  21. data/app/models/hr_lite/approval.rb +5 -0
  22. data/app/models/hr_lite/attendance_record.rb +26 -0
  23. data/app/models/hr_lite/employee_profile.rb +1 -0
  24. data/app/models/hr_lite/hr_request.rb +2 -1
  25. data/app/models/hr_lite/office_location.rb +12 -0
  26. data/app/models/hr_lite/regularization_request.rb +10 -4
  27. data/app/services/hr_lite/attendance_puncher.rb +8 -3
  28. data/app/views/hr_lite/admin/assets/index.html.erb +3 -2
  29. data/app/views/hr_lite/admin/assets/new.html.erb +6 -2
  30. data/app/views/hr_lite/admin/attendances/index.html.erb +37 -6
  31. data/app/views/hr_lite/admin/attendances/show.html.erb +11 -7
  32. data/app/views/hr_lite/admin/attendances/week.html.erb +42 -0
  33. data/app/views/hr_lite/admin/checklists/index.html.erb +1 -1
  34. data/app/views/hr_lite/admin/comp_off_requests/index.html.erb +2 -2
  35. data/app/views/hr_lite/admin/comp_off_requests/show.html.erb +2 -2
  36. data/app/views/hr_lite/admin/documents/index.html.erb +1 -1
  37. data/app/views/hr_lite/admin/employees/_form.html.erb +15 -7
  38. data/app/views/hr_lite/admin/employees/show.html.erb +5 -5
  39. data/app/views/hr_lite/admin/expenses/index.html.erb +2 -2
  40. data/app/views/hr_lite/admin/leave_balances/index.html.erb +3 -3
  41. data/app/views/hr_lite/admin/leave_requests/index.html.erb +2 -2
  42. data/app/views/hr_lite/admin/leave_requests/show.html.erb +4 -6
  43. data/app/views/hr_lite/admin/loans/new.html.erb +2 -1
  44. data/app/views/hr_lite/admin/overview/index.html.erb +3 -3
  45. data/app/views/hr_lite/admin/regularization_requests/index.html.erb +1 -1
  46. data/app/views/hr_lite/admin/shared/_approvals_tabs.html.erb +6 -5
  47. data/app/views/hr_lite/appraisals/show.html.erb +1 -1
  48. data/app/views/hr_lite/approvals/index.html.erb +1 -1
  49. data/app/views/hr_lite/attendance/_month_grid.html.erb +21 -26
  50. data/app/views/hr_lite/attendance/_punch_card.html.erb +32 -10
  51. data/app/views/hr_lite/attendance/show.html.erb +36 -7
  52. data/app/views/hr_lite/benefits/index.html.erb +1 -1
  53. data/app/views/hr_lite/calendar/show.html.erb +29 -22
  54. data/app/views/hr_lite/comp_off_requests/index.html.erb +3 -3
  55. data/app/views/hr_lite/comp_off_requests/new.html.erb +3 -2
  56. data/app/views/hr_lite/documents/index.html.erb +32 -30
  57. data/app/views/hr_lite/employee_profiles/show.html.erb +2 -2
  58. data/app/views/hr_lite/expenses/index.html.erb +17 -35
  59. data/app/views/hr_lite/expenses/new.html.erb +11 -6
  60. data/app/views/hr_lite/holidays/_next_holiday.html.erb +11 -8
  61. data/app/views/hr_lite/holidays/index.html.erb +6 -6
  62. data/app/views/hr_lite/home/index.html.erb +29 -19
  63. data/app/views/hr_lite/hr_requests/index.html.erb +7 -24
  64. data/app/views/hr_lite/hr_requests/new.html.erb +7 -5
  65. data/app/views/hr_lite/hr_requests/show.html.erb +4 -0
  66. data/app/views/hr_lite/kudos/index.html.erb +4 -3
  67. data/app/views/hr_lite/leave_balances/index.html.erb +14 -5
  68. data/app/views/hr_lite/leave_requests/_balance_chips.html.erb +5 -3
  69. data/app/views/hr_lite/leave_requests/_out_list.html.erb +16 -0
  70. data/app/views/hr_lite/leave_requests/index.html.erb +8 -21
  71. data/app/views/hr_lite/leave_requests/new.html.erb +22 -9
  72. data/app/views/hr_lite/leave_requests/show.html.erb +3 -5
  73. data/app/views/hr_lite/loans/index.html.erb +18 -20
  74. data/app/views/hr_lite/policies/index.html.erb +1 -1
  75. data/app/views/hr_lite/regularization_requests/index.html.erb +2 -2
  76. data/app/views/hr_lite/regularization_requests/new.html.erb +11 -8
  77. data/app/views/hr_lite/resignations/show.html.erb +1 -1
  78. data/app/views/hr_lite/salary_slips/_slip_detail.html.erb +9 -3
  79. data/app/views/hr_lite/salary_slips/index.html.erb +48 -44
  80. data/app/views/hr_lite/shared/_bell.html.erb +1 -1
  81. data/app/views/hr_lite/shared/_form_errors.html.erb +1 -1
  82. data/app/views/hr_lite/tax_declarations/show.html.erb +24 -19
  83. data/app/views/hr_lite/team/show.html.erb +2 -2
  84. data/app/views/layouts/hr_lite/application.html.erb +24 -8
  85. data/config/routes.rb +3 -1
  86. data/lib/hr_lite/configuration.rb +16 -2
  87. data/lib/hr_lite/notifications.rb +1 -0
  88. data/lib/hr_lite/version.rb +1 -1
  89. metadata +4 -1
@@ -11,30 +11,17 @@
11
11
  </div>
12
12
  </div>
13
13
 
14
- <%= render "hr_lite/leave_requests/balance_chips" %>
14
+ <%= render "hr_lite/leave_requests/balance_chips", compact: true %>
15
15
 
16
16
  <section class="hrl-card">
17
17
  <% if @requests.any? %>
18
- <div class="hrl-table-wrap">
19
- <table class="hrl-table hrl-table--stack">
20
- <thead><tr><th>Type</th><th>Dates</th><th>Days</th><th>Status</th><th></th></tr></thead>
21
- <tbody>
22
- <% @requests.each do |request| %>
23
- <tr>
24
- <td data-label="Type"><span class="hrl-badge" style="background: <%= request.leave_type.color %>1f; color: <%= request.leave_type.color %>;"><%= request.leave_type.code %></span></td>
25
- <td data-label="Dates"><%= request.date_range_label %></td>
26
- <td data-label="Days" class="hrl-num"><%= request.days_count.to_f %></td>
27
- <td data-label="Status">
28
- <span class="hrl-badge <%= { "approved" => "hrl-badge--ok", "rejected" => "hrl-badge--bad", "cancelled" => "hrl-badge--muted" }[request.status] %>"><%= request.status.humanize %></span>
29
- </td>
30
- <td data-label="">
31
- <a class="hrl-small" href="<%= hr_lite.leave_request_path(request) %>">View</a>
32
- </td>
33
- </tr>
34
- <% end %>
35
- </tbody>
36
- </table>
37
- </div>
18
+ <% @requests.each do |request| %>
19
+ <a class="hrl-listrow" href="<%= hr_lite.leave_request_path(request) %>">
20
+ <span><span class="hrl-badge hrl-badge--type" style="--hrl-type: <%= request.leave_type.color %>"><%= request.leave_type.code %></span> <%= request.leave_type.name %></span>
21
+ <%= hrl_request_status_badge(request.status) %>
22
+ <span class="hrl-small hrl-muted"><%= request.date_range_label %> · <%= pluralize(hrl_days(request.days_count), "day") %></span>
23
+ </a>
24
+ <% end %>
38
25
  <%= hrl_pagination %>
39
26
  <% else %>
40
27
  <p class="hrl-card--empty">No leave requests yet.</p>
@@ -1,29 +1,37 @@
1
1
  <% content_for(:page_title) { "Apply for leave" } %>
2
+ <% content_for(:back, "Leaves") %>
2
3
  <div class="hrl-page__head">
3
4
  <h1 class="hrl-page__title">Apply for leave</h1>
4
5
  </div>
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>
8
-
9
7
  <section class="hrl-card">
10
8
  <%= form_with model: @request, url: hr_lite.leave_requests_path, local: true do |f| %>
11
9
  <%= render "hr_lite/shared/form_errors", record: @request %>
12
10
  <div class="hrl-field">
13
- <%= f.label :leave_type_id, "Type" %>
14
- <%= f.collection_select :leave_type_id, HrLite::LeaveType.active, :id, :name %>
11
+ <%= f.label :leave_type_id, "Type", id: "hrl-leave-type", for: nil %>
12
+ <% left = @balances.index_by(&:leave_type_id) %>
13
+ <div class="hrl-chiprow" role="radiogroup" aria-labelledby="hrl-leave-type">
14
+ <% HrLite::LeaveType.active.each_with_index do |type, i| %>
15
+ <label><%= f.radio_button :leave_type_id, type.id, required: i.zero? %><span><%= type.name.sub(/ leave\z/i, "") %><%= " · #{hrl_days(left[type.id].available)} left" if left[type.id] %><%= " · Unpaid" unless type.paid %></span></label>
16
+ <% end %>
17
+ </div>
18
+ <p class="hrl-hint">Days beyond your balance are unpaid (loss of pay), cut from that month's salary.</p>
15
19
  </div>
16
20
  <div class="hrl-field">
17
21
  <%= f.label :start_date, "From" %>
18
- <%= f.date_field :start_date %>
22
+ <%= f.date_field :start_date, required: true %>
19
23
  </div>
20
24
  <div class="hrl-field">
21
- <%= f.label :end_date, "To" %>
25
+ <%= f.label :end_date, "To (leave blank for one day)" %>
22
26
  <%= f.date_field :end_date %>
23
27
  </div>
24
28
  <div class="hrl-field">
25
- <%= f.label :half_day_part, "Day" %>
26
- <%= f.select :half_day_part, HrLite::DAY_PARTS %>
29
+ <%= f.label :half_day_part, "Day", id: "hrl-day-part", for: nil %>
30
+ <div class="hrl-chiprow" role="radiogroup" aria-labelledby="hrl-day-part">
31
+ <% HrLite::DAY_PARTS.each do |label, value| %>
32
+ <label><%= f.radio_button :half_day_part, value %><span><%= label %></span></label>
33
+ <% end %>
34
+ </div>
27
35
  </div>
28
36
  <div class="hrl-field">
29
37
  <%= f.label :reason, "Reason (optional)" %>
@@ -35,3 +43,8 @@
35
43
  </div>
36
44
  <% end %>
37
45
  </section>
46
+
47
+ <section class="hrl-card">
48
+ <h2 class="hrl-card__title">Out in the next 2 weeks</h2>
49
+ <%= render "hr_lite/leave_requests/out_list", leaves: @out_soon, empty: "Nobody is on leave in the next 2 weeks." %>
50
+ </section>
@@ -1,17 +1,15 @@
1
1
  <% content_for(:page_title) { "Leave request" } %>
2
+ <% content_for(:back, "Leaves") %>
2
3
  <div class="hrl-page__head">
3
4
  <h1 class="hrl-page__title">Leave request</h1>
4
- <div class="hrl-page__actions">
5
- <a class="hrl-btn" href="<%= hr_lite.leave_requests_path %>">Back</a>
6
- </div>
7
5
  </div>
8
6
 
9
7
  <section class="hrl-card">
10
8
  <dl class="hrl-deflist">
11
9
  <dt>Type</dt><dd><%= @request.leave_type.name %></dd>
12
10
  <dt>Dates</dt><dd><%= @request.date_range_label %></dd>
13
- <dt>Days</dt><dd><%= @request.days_count.to_f %></dd>
14
- <dt>Status</dt><dd><span class="hrl-badge"><%= @request.status.humanize %></span></dd>
11
+ <dt>Days</dt><dd><%= hrl_days(@request.days_count) %></dd>
12
+ <dt>Status</dt><dd><%= hrl_request_status_badge(@request.status) %></dd>
15
13
  <% if @request.reason.present? %><dt>Reason</dt><dd><%= @request.reason %></dd><% end %>
16
14
  <% if @request.decision_note.present? %><dt>Decision note</dt><dd><%= @request.decision_note %></dd><% end %>
17
15
  <% if @request.decided_at %><dt>Decided</dt><dd><%= @request.decided_at.strftime("%d %b %Y") %> by <%= hr_display_name(@request.decided_by) %></dd><% end %>
@@ -2,7 +2,10 @@
2
2
  <div class="hrl-page__head"><h1 class="hrl-page__title">Loans &amp; advances</h1></div>
3
3
 
4
4
  <% if @loans.empty? %>
5
- <section class="hrl-card"><p class="hrl-muted">You have no loans or salary advances.</p></section>
5
+ <section class="hrl-card">
6
+ <p class="hrl-muted">You have no loans or salary advances.</p>
7
+ <a class="hrl-card__link" href="<%= hr_lite.new_hr_request_path(category: "payroll_query") %>">Ask HR about an advance</a>
8
+ </section>
6
9
  <% else %>
7
10
  <section class="hrl-card">
8
11
  <p>Outstanding across everything: <strong><%= hrl_money(@outstanding) %></strong></p>
@@ -15,32 +18,27 @@
15
18
  <span class="hrl-badge <%= "hrl-badge--muted" unless loan.active? %>"><%= loan.status.humanize %></span>
16
19
  </h2>
17
20
  <% if loan.reason.present? %><p class="hrl-muted"><%= loan.reason %></p><% end %>
18
- <div class="hrl-table-wrap">
19
- <table class="hrl-table hrl-table--stack">
20
- <tbody>
21
- <tr><td data-label="Monthly instalment"><%= hrl_money(loan.monthly_instalment) %></td></tr>
22
- <tr><td data-label="Repaid so far"><%= hrl_money(loan.repaid) %></td></tr>
23
- <tr><td data-label="Outstanding"><strong><%= hrl_money(loan.outstanding) %></strong></td></tr>
24
- <tr>
25
- <td data-label="Next deduction">
26
- <% amount = loan.instalment_for(@next_month) %>
27
- <%= amount.positive? ? "#{hrl_money(amount)} in #{@next_month.strftime('%b %Y')}" : "—" %>
28
- </td>
29
- </tr>
30
- </tbody>
31
- </table>
32
- </div>
21
+ <dl class="hrl-deflist">
22
+ <dt>Monthly instalment</dt><dd><%= hrl_money(loan.monthly_instalment) %></dd>
23
+ <dt>Repaid so far</dt><dd><%= hrl_money(loan.repaid) %></dd>
24
+ <dt>Outstanding</dt><dd><strong><%= hrl_money(loan.outstanding) %></strong></dd>
25
+ <dt>Next deduction</dt>
26
+ <dd>
27
+ <% amount = loan.instalment_for(@next_month) %>
28
+ <%= amount.positive? ? "#{hrl_money(amount)} in #{@next_month.strftime('%b %Y')}" : "—" %>
29
+ </dd>
30
+ </dl>
33
31
 
34
32
  <% if loan.loan_repayments.any? %>
35
33
  <h3 class="hrl-card__title">Already deducted</h3>
36
34
  <div class="hrl-table-wrap">
37
- <table class="hrl-table hrl-table--stack">
38
- <thead><tr><th>Month</th><th>Amount</th></tr></thead>
35
+ <table class="hrl-table">
36
+ <thead><tr><th>Month</th><th class="hrl-num">Amount</th></tr></thead>
39
37
  <tbody>
40
38
  <% loan.loan_repayments.sort_by(&:period_month).reverse.each do |r| %>
41
39
  <tr>
42
- <td data-label="Month"><%= r.period_month.strftime("%b %Y") %></td>
43
- <td data-label="Amount" class="hrl-num"><%= hrl_money(r.amount) %></td>
40
+ <td><%= r.period_month.strftime("%b %Y") %></td>
41
+ <td class="hrl-num"><%= hrl_money(r.amount) %></td>
44
42
  </tr>
45
43
  <% end %>
46
44
  </tbody>
@@ -12,7 +12,7 @@
12
12
  <% @policies.each do |policy| %>
13
13
  <tr>
14
14
  <td data-label="Policy">
15
- <a href="<%= hr_lite.policy_path(policy) %>"><%= policy.title %></a>
15
+ <a class="hrl-btn" href="<%= hr_lite.policy_path(policy) %>"><%= policy.title %></a>
16
16
  <span class="hrl-badge hrl-badge--muted">v<%= policy.version %></span>
17
17
  </td>
18
18
  <td data-label="In force"><%= policy.effective_from.strftime("%d %b %Y") %></td>
@@ -1,14 +1,14 @@
1
1
  <% content_for(:page_title) { "Regularization" } %>
2
+ <% content_for(:back, "Attendance") %>
2
3
  <div class="hrl-page__head">
3
4
  <h1 class="hrl-page__title">My regularization tickets</h1>
4
5
  <div class="hrl-page__actions">
5
- <a class="hrl-btn" href="<%= hr_lite.attendance_path %>">My attendance</a>
6
6
  <a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_regularization_request_path %>">Raise a ticket</a>
7
7
  </div>
8
8
  </div>
9
9
 
10
10
  <section class="hrl-card">
11
- <p class="hrl-small hrl-muted" style="margin-top:0;">
11
+ <p class="hrl-small hrl-muted">
12
12
  Forgot to check in or out? Raise a ticket with the actual times — once an
13
13
  admin approves, your attendance for that day is fixed automatically.
14
14
  </p>
@@ -1,4 +1,5 @@
1
1
  <% content_for(:page_title) { "Raise a regularization ticket" } %>
2
+ <% content_for(:back, "Attendance") %>
2
3
  <div class="hrl-page__head">
3
4
  <h1 class="hrl-page__title">Raise a regularization ticket</h1>
4
5
  </div>
@@ -7,23 +8,25 @@
7
8
  <%= form_with model: @request, url: hr_lite.regularization_requests_path, scope: :regularization_request, local: true do |f| %>
8
9
  <%= render "hr_lite/shared/form_errors", record: @request %>
9
10
  <% if (rule = HrLite.config.self_regularization) %>
10
- <p class="hrl-small hrl-muted">A missed punch on a working day from today back to <%= rule[:within_days] %> days ago is fixed as soon as you submit, up to <%= rule[:per_week] %> times a week. Anything else goes to HR as a ticket.</p>
11
+ <p class="hrl-small hrl-muted">A missed punch or short day on a working day from today back to <%= rule[:within_days] %> days ago is fixed as soon as you submit, up to <%= rule[:per_week] %> times a week. Anything else goes to HR as a ticket.</p>
11
12
  <% end %>
12
13
  <div class="hrl-field">
13
14
  <%= f.label :date, "Which day needs fixing?" %>
14
- <%= f.date_field :date, value: @request.date&.strftime("%Y-%m-%d"), max: Date.current %>
15
+ <%= f.date_field :date, value: @request.date&.strftime("%Y-%m-%d"), max: Date.current, required: true %>
15
16
  </div>
16
17
  <div class="hrl-field">
17
- <%= f.label :check_in_at, "Actual check-in (leave blank if only check-out is missing)" %>
18
- <%= f.datetime_local_field :check_in_at, value: @request.check_in_at&.strftime("%Y-%m-%dT%H:%M"), max: Date.current.strftime("%Y-%m-%dT23:59") %>
18
+ <%= f.label :check_in_at, "Actual check-in" %>
19
+ <%= f.time_field :check_in_at, value: @request.check_in_at&.strftime("%H:%M") %>
20
+ <p class="hrl-hint">Leave blank if only check-out is missing.</p>
19
21
  </div>
20
22
  <div class="hrl-field">
21
- <%= f.label :check_out_at, "Actual check-out (leave blank if only check-in is missing)" %>
22
- <%= f.datetime_local_field :check_out_at, value: @request.check_out_at&.strftime("%Y-%m-%dT%H:%M"), max: Date.current.strftime("%Y-%m-%dT23:59") %>
23
+ <%= f.label :check_out_at, "Actual check-out" %>
24
+ <%= f.time_field :check_out_at, value: @request.check_out_at&.strftime("%H:%M") %>
25
+ <p class="hrl-hint">Leave blank if only check-in is missing. Short day? Leave both times blank and give the reason.</p>
23
26
  </div>
24
27
  <div class="hrl-field">
25
- <%= f.label :reason, "Why was the punch missed? (required)" %>
26
- <%= f.text_area :reason %>
28
+ <%= f.label :reason, "Why does this day need fixing? (required)" %>
29
+ <%= f.text_area :reason, required: true %>
27
30
  </div>
28
31
  <div class="hrl-form-actions">
29
32
  <%= f.submit "Raise ticket", class: "hrl-btn hrl-btn--primary" %>
@@ -28,7 +28,7 @@
28
28
  <%= render "hr_lite/shared/form_errors", record: @new_resignation %>
29
29
  <div class="hrl-field">
30
30
  <%= f.label :proposed_last_day, "Proposed last working day" %>
31
- <%= f.date_field :proposed_last_day %>
31
+ <%= f.date_field :proposed_last_day, required: true %>
32
32
  </div>
33
33
  <div class="hrl-field">
34
34
  <%= f.label :reason, "Reason (optional)" %>
@@ -1,4 +1,10 @@
1
1
  <%# locals: slip, profile — shared by employee show, admin show %>
2
+ <div class="hrl-kpis hrl-kpis--money">
3
+ <div class="hrl-kpi"><div class="hrl-kpi__value"><%= hrl_money(slip.net_pay) %></div><div class="hrl-kpi__label">Net pay</div></div>
4
+ <div class="hrl-kpi"><div class="hrl-kpi__value"><%= hrl_money(slip.gross_earnings) %></div><div class="hrl-kpi__label">Gross</div></div>
5
+ <div class="hrl-kpi"><div class="hrl-kpi__value"><%= hrl_money(slip.total_deductions) %></div><div class="hrl-kpi__label">Deductions</div></div>
6
+ <div class="hrl-kpi"><div class="hrl-kpi__value"><%= hrl_days(slip.payable_days) %> / <%= hrl_days(slip.effective_lop_days) %></div><div class="hrl-kpi__label">Payable / LOP days</div></div>
7
+ </div>
2
8
  <div class="hrl-grid hrl-grid--2">
3
9
  <section class="hrl-card">
4
10
  <h2 class="hrl-card__title">Earnings</h2>
@@ -36,12 +42,12 @@
36
42
 
37
43
  <section class="hrl-card">
38
44
  <dl class="hrl-deflist">
39
- <dt>Net pay</dt><dd><strong><%= hrl_money(slip.net_pay) %></strong> (<%= hrl_amount_in_words(slip.net_pay) %>)</dd>
45
+ <dt>Net pay</dt><dd><strong><%= hrl_money(slip.net_pay) %></strong><br><span class="hrl-small hrl-muted"><%= hrl_amount_in_words(slip.net_pay) %></span></dd>
40
46
  <%# Out-of-window days are named rather than silently dropped: without them
41
47
  a mid-month joiner's row does not add up to the month. %>
42
48
  <dt>Days</dt>
43
- <dd><%= slip.payable_days %> payable / <%= slip.effective_lop_days %> LOP<% if slip.out_of_window_days.to_f.positive? %>
44
- / <%= slip.out_of_window_days %> outside employment<% end %> of <%= slip.days_in_month %></dd>
49
+ <dd><%= hrl_days(slip.payable_days) %> payable / <%= hrl_days(slip.effective_lop_days) %> LOP<% if slip.out_of_window_days.to_f.positive? %>
50
+ / <%= hrl_days(slip.out_of_window_days) %> outside employment<% end %> of <%= slip.days_in_month %></dd>
45
51
  <% if profile %>
46
52
  <dt>Employee code</dt><dd><%= profile.employee_code %></dd>
47
53
  <% if profile.masked_uan %><dt>PF UAN</dt><dd><%= profile.masked_uan %></dd><% end %>
@@ -4,31 +4,60 @@
4
4
  </div>
5
5
 
6
6
  <section class="hrl-card">
7
- <h2 class="hrl-card__title">My salary</h2>
8
- <% if (breakup = @structure&.breakup) %>
9
- <p class="hrl-small hrl-muted">Current salary from <%= @structure.effective_from.strftime("%d %b %Y") %></p>
7
+ <h2 class="hrl-card__title">Slips</h2>
8
+ <% if @slips.any? %>
10
9
  <div class="hrl-table-wrap">
11
- <%# Three narrow columns fit a 320px phone; stacking repeated the headings on every line. %>
12
- <table class="hrl-table hrl-table--tight hrl-small">
13
- <thead><tr><th>Component</th><th class="hrl-num">Monthly</th><th class="hrl-num">Yearly</th></tr></thead>
10
+ <table class="hrl-table hrl-table--stack">
11
+ <thead><tr><th>Period</th><th class="hrl-num">Net pay</th><th></th></tr></thead>
14
12
  <tbody>
15
- <% { "Earnings" => breakup[:earnings].merge("Gross" => breakup[:monthly_gross]),
16
- "Deductions" => breakup[:deductions].merge("In-hand before tax" => breakup[:in_hand]),
17
- "Employer contributions" => breakup[:employer].merge("CTC" => breakup[:monthly_ctc]) }.each do |section, lines| %>
18
- <tr><th colspan="3"><%= section %></th></tr>
19
- <% lines.each do |label, amount| %>
20
- <% weight = label == lines.keys.last ? :strong : :span %>
21
- <tr>
22
- <td data-label="Component"><%= content_tag(weight, label) %></td>
23
- <td data-label="Monthly" class="hrl-num"><%= content_tag(weight, hrl_rupees(amount)) %></td>
24
- <td data-label="Yearly" class="hrl-num"><%= content_tag(weight, hrl_rupees(breakup[:yearly].fetch(label, amount * 12))) %></td>
25
- </tr>
26
- <% end %>
13
+ <% @slips.each do |slip| %>
14
+ <tr>
15
+ <td data-label="Period"><%= slip.period_month.strftime("%B %Y") %></td>
16
+ <td data-label="Net pay" class="hrl-num"><strong><%= hrl_money(slip.net_pay) %></strong></td>
17
+ <td data-label="">
18
+ <a class="hrl-btn" href="<%= hr_lite.salary_slip_path(slip) %>">View</a>
19
+ <a class="hrl-btn" href="<%= hr_lite.salary_slip_path(slip, format: :pdf) %>">PDF</a>
20
+ </td>
21
+ </tr>
27
22
  <% end %>
28
23
  </tbody>
29
24
  </table>
30
25
  </div>
31
- <p class="hrl-small hrl-muted">Income tax (TDS) depends on your declarations and is shown on each slip.</p>
26
+ <%= hrl_pagination %>
27
+ <% else %>
28
+ <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>
29
+ <% end %>
30
+ </section>
31
+
32
+ <section class="hrl-card">
33
+ <h2 class="hrl-card__title">My salary</h2>
34
+ <% if (breakup = @structure&.breakup) %>
35
+ <p class="hrl-small hrl-muted">Current salary from <%= @structure.effective_from.strftime("%d %b %Y") %></p>
36
+ <details>
37
+ <summary class="hrl-btn">Show salary structure</summary>
38
+ <div class="hrl-table-wrap hrl-mt">
39
+ <%# Three narrow columns fit a 320px phone; stacking repeated the headings on every line. %>
40
+ <table class="hrl-table hrl-table--tight hrl-small">
41
+ <thead><tr><th>Component</th><th class="hrl-num">Monthly</th><th class="hrl-num">Yearly</th></tr></thead>
42
+ <tbody>
43
+ <% { "Earnings" => breakup[:earnings].merge("Gross" => breakup[:monthly_gross]),
44
+ "Deductions" => breakup[:deductions].merge("In-hand before tax" => breakup[:in_hand]),
45
+ "Employer contributions" => breakup[:employer].merge("CTC" => breakup[:monthly_ctc]) }.each do |section, lines| %>
46
+ <tr><th colspan="3"><%= section %></th></tr>
47
+ <% lines.each do |label, amount| %>
48
+ <% weight = label == lines.keys.last ? :strong : :span %>
49
+ <tr>
50
+ <td data-label="Component"><%= content_tag(weight, label) %></td>
51
+ <td data-label="Monthly" class="hrl-num"><%= content_tag(weight, hrl_rupees(amount)) %></td>
52
+ <td data-label="Yearly" class="hrl-num"><%= content_tag(weight, hrl_rupees(breakup[:yearly].fetch(label, amount * 12))) %></td>
53
+ </tr>
54
+ <% end %>
55
+ <% end %>
56
+ </tbody>
57
+ </table>
58
+ </div>
59
+ <p class="hrl-small hrl-muted">Income tax (TDS) depends on your declarations and is shown on each slip.</p>
60
+ </details>
32
61
  <% end %>
33
62
  <% @upcoming.each do |structure| %>
34
63
  <% upcoming = structure.breakup(on: structure.effective_from) %>
@@ -49,28 +78,3 @@
49
78
  <% end %>
50
79
  <% end %>
51
80
  </section>
52
-
53
- <section class="hrl-card">
54
- <% if @slips.any? %>
55
- <div class="hrl-table-wrap">
56
- <table class="hrl-table hrl-table--stack">
57
- <thead><tr><th>Period</th><th>Net pay</th><th></th></tr></thead>
58
- <tbody>
59
- <% @slips.each do |slip| %>
60
- <tr>
61
- <td data-label="Period"><%= slip.period_month.strftime("%B %Y") %></td>
62
- <td data-label="Net pay" class="hrl-num"><strong><%= hrl_money(slip.net_pay) %></strong></td>
63
- <td data-label="">
64
- <a class="hrl-btn" href="<%= hr_lite.salary_slip_path(slip) %>">View</a>
65
- <a class="hrl-btn" href="<%= hr_lite.salary_slip_path(slip, format: :pdf) %>">PDF</a>
66
- </td>
67
- </tr>
68
- <% end %>
69
- </tbody>
70
- </table>
71
- </div>
72
- <%= hrl_pagination %>
73
- <% else %>
74
- <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>
75
- <% end %>
76
- </section>
@@ -1,3 +1,3 @@
1
1
  <% if (bell = HrLite.config.notifications&.call(hr_current_user)) %>
2
- <a class="hrl-bell" href="<%= bell[:url] %>">Notifications<% if bell[:unread].to_i.positive? %> <span class="hrl-bell__count"><%= bell[:unread] %></span><% end %></a>
2
+ <a class="hrl-bell" href="<%= bell[:url] %>">Notifications<% if bell[:unread].to_i.positive? %> <span class="hrl-bell__count"><%= bell[:unread] %><span class="hrl-sr-only"> unread</span></span><% end %></a>
3
3
  <% end %>
@@ -1,5 +1,5 @@
1
1
  <% if record.errors.any? %>
2
- <div class="hrl-errors">
2
+ <div class="hrl-errors" role="alert" tabindex="-1" id="hrl-errors" autofocus>
3
3
  <strong><%= pluralize(record.errors.count, "problem") %> prevented saving:</strong>
4
4
  <ul>
5
5
  <% record.errors.full_messages.each do |message| %>
@@ -1,4 +1,5 @@
1
1
  <% content_for(:page_title) { "Tax declaration" } %>
2
+ <% submittable = @declaration.persisted? && (@declaration.draft? || @declaration.rejected?) %>
2
3
  <div class="hrl-page__head">
3
4
  <h1 class="hrl-page__title">Tax declaration — <%= @declaration.label %></h1>
4
5
  </div>
@@ -24,24 +25,29 @@
24
25
 
25
26
  <section class="hrl-card">
26
27
  <div class="hrl-field">
27
- <%= f.label :regime, "Tax regime" %>
28
- <%= f.select :regime, [ [ "New regime (lower rates, no deductions)", "new" ],
29
- [ "Old regime (deductions apply)", "old" ] ] %>
30
- <span class="hrl-small hrl-muted">
31
- The deductions below only reduce tax under the old regime.
32
- </span>
28
+ <%= f.label :regime, "Tax regime", id: "hrl-tax-regime", for: nil %>
29
+ <div role="radiogroup" aria-labelledby="hrl-tax-regime">
30
+ <% [ [ "new", "New regime", "Lower rates, no deductions." ],
31
+ [ "old", "Old regime", "The deductions below reduce your tax." ] ].each do |value, name, hint| %>
32
+ <div class="hrl-field--check">
33
+ <%= f.radio_button :regime, value %>
34
+ <%= f.label :regime, value: value do %><span><%= name %><span class="hrl-hint"><%= hint %></span></span><% end %>
35
+ </div>
36
+ <% end %>
37
+ </div>
33
38
  </div>
34
39
  </section>
35
40
 
36
- <section class="hrl-card">
37
- <h2 class="hrl-card__title">What you are claiming</h2>
41
+ <details class="hrl-card" <%= "open" if @declaration.regime != "new" || @declaration.errors.any? %>>
42
+ <summary class="hrl-btn">Deductions (old regime only)</summary>
38
43
  <%= f.fields_for :tax_declaration_items do |item| %>
39
44
  <div class="hrl-field">
40
45
  <%= item.hidden_field :section %>
41
46
  <%= item.label :declared_amount, item.object.section_label %>
42
47
  <%= item.text_field :declared_amount,
43
48
  value: item.object.declared_amount&.to_s("F"), inputmode: "decimal" %>
44
- <%= item.text_field :label, placeholder: "What it is (optional)" %>
49
+ <%= item.label :label, "What it is (optional)", class: "hrl-mt hrl-muted" %>
50
+ <%= item.text_field :label %>
45
51
  <% if item.object.persisted? && item.object.verified_amount %>
46
52
  <span class="hrl-small hrl-muted">
47
53
  HR verified <%= hrl_money(item.object.verified_amount) %>
@@ -50,19 +56,18 @@
50
56
  </div>
51
57
  <% end %>
52
58
  <p>Total claimed: <strong><%= hrl_money(@declaration.declared_total) %></strong></p>
53
- <div class="hrl-form-actions">
54
- <%= f.submit "Save draft", class: "hrl-btn hrl-btn--primary" %>
55
- </div>
56
- </section>
57
- <% end %>
59
+ </details>
58
60
 
59
- <% if @declaration.persisted? && (@declaration.draft? || @declaration.rejected?) %>
60
- <section class="hrl-card">
61
+ <% if submittable %>
61
62
  <p class="hrl-muted hrl-small">
62
63
  Submitting locks it for HR to check. They can send it back if the proof
63
64
  does not match.
64
65
  </p>
65
- <%= button_to "Submit to HR", hr_lite.submit_tax_declaration_path,
66
- method: :post, class: "hrl-btn hrl-btn--primary" %>
67
- </section>
66
+ <% end %>
67
+ <div class="hrl-form-actions">
68
+ <%= f.submit "Save draft", class: submittable ? "hrl-btn" : "hrl-btn hrl-btn--primary" %>
69
+ <% if submittable %>
70
+ <button type="submit" name="submit_to_hr" value="1" class="hrl-btn hrl-btn--primary">Submit to HR</button>
71
+ <% end %>
72
+ </div>
68
73
  <% end %>
@@ -33,7 +33,7 @@
33
33
  <span class="hrl-small hrl-muted"> · <%= row.profile.designation %></span><% end %></span>
34
34
  </td>
35
35
  <td data-label="Status"><%= hrl_team_status(row) %></td>
36
- <td data-label="Hours" class="hrl-num"><%= hrl_duration(row.day_seconds) %></td>
36
+ <td data-label="Hours <%= @date == Date.current ? "today" : "that day" %>" class="hrl-num"><%= hrl_duration(row.day_seconds) %></td>
37
37
  <td data-label="Hours this month" class="hrl-num"><%= hrl_duration(row.month_seconds) %></td>
38
38
  </tr>
39
39
  <% end %>
@@ -44,5 +44,5 @@
44
44
  <p class="hrl-card--empty">Nobody to show for this date.</p>
45
45
  <% end %>
46
46
  <p class="hrl-small hrl-muted hrl-mt">Forgot a punch yourself?
47
- <a href="<%= hr_lite.new_regularization_request_path %>">Raise a regularization ticket</a>.</p>
47
+ <a class="hrl-card__link" href="<%= hr_lite.new_regularization_request_path %>">Raise a regularization ticket</a></p>
48
48
  </section>
@@ -13,6 +13,7 @@
13
13
  <% end %>
14
14
  </head>
15
15
  <body class="hrl-body">
16
+ <a class="hrl-skip" href="#hrl-main">Skip to content</a>
16
17
  <div class="hrl-shell">
17
18
  <%# Keka-style left rail on desktop; slides away on phones (tabbar takes over). %>
18
19
  <aside class="hrl-side" aria-label="HR navigation">
@@ -25,9 +26,11 @@
25
26
  </a>
26
27
 
27
28
  <nav class="hrl-side__nav">
28
- <span class="hrl-side__group">My work</span>
29
- <% hrl_nav_items(HrLite::ApplicationHelper::NAV_ITEMS).each do |item| %>
30
- <%= hrl_nav_link(item) %>
29
+ <% hrl_nav_items(HrLite::ApplicationHelper::NAV_ITEMS).chunk { |item| item[:group] }.each do |group, items| %>
30
+ <span class="hrl-side__group"><%= group %></span>
31
+ <% items.each do |item| %>
32
+ <%= hrl_nav_link(item) %>
33
+ <% end %>
31
34
  <% end %>
32
35
 
33
36
  <% if hr_admin? || hr_leadership? || hr_can?("expense.approve", scope: :team) %>
@@ -73,10 +76,15 @@
73
76
  <% end %>
74
77
  </a>
75
78
  <%= render "hr_lite/shared/bell" %>
76
- <span class="hrl-appbar__user"><%= hr_display_name(hr_current_user) %></span>
79
+ <% profile = hr_lite.employee_profile_path if hr_lite_route?(:employee_profile_path) %>
80
+ <%= tag.a hr_display_name(hr_current_user), href: profile, class: "hrl-appbar__user" %>
77
81
  </header>
78
82
 
79
- <main class="hrl-main">
83
+ <main class="hrl-main" id="hrl-main" tabindex="-1">
84
+ <%# A sub-screen names its parent tab: content_for(:back, "Leaves"). %>
85
+ <% if (tab = HrLite::ApplicationHelper::NAV_ITEMS.find { |i| i[:label] == content_for(:back) }) %>
86
+ <a class="hrl-backlink" href="<%= hr_lite.public_send(tab[:path]) %>"><span aria-hidden="true">‹</span> <%= tab[:label] %></a>
87
+ <% end %>
80
88
  <% if flash[:notice].present? %><div class="hrl-flash hrl-flash--ok" role="status"><%= flash[:notice] %></div><% end %>
81
89
  <% if flash[:alert].present? %><div class="hrl-flash hrl-flash--warn" role="alert"><%= flash[:alert] %></div><% end %>
82
90
  <%= yield %>
@@ -95,10 +103,17 @@
95
103
  <%= hrl_nav_link(item) %>
96
104
  <% end %>
97
105
  <details class="hrl-tabbar__more">
98
- <summary class="hrl-nav__link">More</summary>
106
+ <% lit = rest.any? { |i| hrl_nav_active?(i) } || request.path.include?("/admin/") %>
107
+ <%= tag.summary "More", class: [ "hrl-nav__link", ("hrl-nav__link--active" if lit) ], aria: { current: ("true" if lit) } %>
99
108
  <div class="hrl-sheet">
100
- <% rest.each do |item| %>
101
- <%= hrl_nav_link(item) %>
109
+ <% if HrLite.config.back_link %>
110
+ <a class="hrl-nav__link hrl-side__back" href="<%= HrLite.config.back_link[:url] %>"><%= HrLite.config.back_link[:label] %></a>
111
+ <% end %>
112
+ <% rest.chunk { |item| item[:group] }.each do |group, items| %>
113
+ <span class="hrl-side__group"><%= group %></span>
114
+ <% items.each do |item| %>
115
+ <%= hrl_nav_link(item) %>
116
+ <% end %>
102
117
  <% end %>
103
118
  <% if hr_admin? || hr_leadership? || hr_can?("expense.approve", scope: :team) %>
104
119
  <span class="hrl-side__group">Manage</span>
@@ -126,6 +141,7 @@
126
141
  <%= javascript_include_tag "hr_lite/confirm", defer: true %>
127
142
  <%= javascript_include_tag "hr_lite/mention", defer: true %>
128
143
  <%= javascript_include_tag "hr_lite/nav", defer: true %>
144
+ <%= javascript_include_tag "hr_lite/select_filter", defer: true %>
129
145
  <%= yield :scripts %>
130
146
  </body>
131
147
  </html>
data/config/routes.rb CHANGED
@@ -54,7 +54,9 @@ HrLite::Engine.routes.draw do
54
54
  namespace :admin do
55
55
  resources :policies, only: %i[index new create]
56
56
  get "overview", to: "overview#index"
57
- resources :attendances, only: %i[index show update], param: :user_id
57
+ resources :attendances, only: %i[index show update], param: :user_id do
58
+ collection { get :week }
59
+ end
58
60
  resources :leave_requests, only: %i[index show new create] do
59
61
  member { post :approve; post :reject; post :cancel }
60
62
  end