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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +5 -2
  4. data/app/assets/javascripts/hr_lite/emoji_pick.js +4 -4
  5. data/app/assets/stylesheets/hr_lite/hr_lite.css +7 -6
  6. data/app/controllers/hr_lite/admin/attendances_controller.rb +2 -0
  7. data/app/controllers/hr_lite/admin/comp_off_requests_controller.rb +1 -1
  8. data/app/controllers/hr_lite/admin/documents_controller.rb +1 -1
  9. data/app/controllers/hr_lite/admin/expenses_controller.rb +13 -1
  10. data/app/controllers/hr_lite/admin/leave_requests_controller.rb +1 -1
  11. data/app/controllers/hr_lite/admin/regularization_requests_controller.rb +1 -1
  12. data/app/controllers/hr_lite/admin/statutory_rate_cards_controller.rb +2 -1
  13. data/app/controllers/hr_lite/admin/tax_declarations_controller.rb +2 -1
  14. data/app/controllers/hr_lite/admin/work_locations_controller.rb +1 -1
  15. data/app/controllers/hr_lite/home_controller.rb +0 -9
  16. data/app/controllers/hr_lite/loans_controller.rb +1 -1
  17. data/app/helpers/hr_lite/application_helper.rb +21 -2
  18. data/app/models/hr_lite/attendance_record.rb +7 -4
  19. data/app/models/hr_lite/salary_slip.rb +2 -1
  20. data/app/models/hr_lite/statutory_rate_card_record.rb +6 -0
  21. data/app/views/hr_lite/admin/attendances/index.html.erb +4 -4
  22. data/app/views/hr_lite/admin/attendances/show.html.erb +1 -1
  23. data/app/views/hr_lite/admin/benefits/index.html.erb +10 -4
  24. data/app/views/hr_lite/admin/comp_off_requests/show.html.erb +1 -1
  25. data/app/views/hr_lite/admin/documents/index.html.erb +2 -2
  26. data/app/views/hr_lite/admin/employees/show.html.erb +2 -1
  27. data/app/views/hr_lite/admin/expense_categories/index.html.erb +1 -1
  28. data/app/views/hr_lite/admin/expenses/index.html.erb +4 -2
  29. data/app/views/hr_lite/admin/holidays/index.html.erb +2 -1
  30. data/app/views/hr_lite/admin/hr_requests/index.html.erb +2 -2
  31. data/app/views/hr_lite/admin/hr_requests/show.html.erb +1 -1
  32. data/app/views/hr_lite/admin/leave_requests/show.html.erb +1 -1
  33. data/app/views/hr_lite/admin/leave_types/index.html.erb +2 -2
  34. data/app/views/hr_lite/admin/loans/new.html.erb +1 -1
  35. data/app/views/hr_lite/admin/office_locations/index.html.erb +1 -1
  36. data/app/views/hr_lite/admin/overview/index.html.erb +10 -31
  37. data/app/views/hr_lite/admin/payroll_line_items/new.html.erb +2 -1
  38. data/app/views/hr_lite/admin/payroll_runs/index.html.erb +1 -1
  39. data/app/views/hr_lite/admin/payroll_runs/show.html.erb +6 -4
  40. data/app/views/hr_lite/admin/performance_plans/_form.html.erb +2 -1
  41. data/app/views/hr_lite/admin/policies/index.html.erb +1 -1
  42. data/app/views/hr_lite/admin/policies/new.html.erb +2 -1
  43. data/app/views/hr_lite/admin/regularization_requests/show.html.erb +1 -1
  44. data/app/views/hr_lite/admin/roles/_form.html.erb +2 -1
  45. data/app/views/hr_lite/admin/roles/edit.html.erb +4 -2
  46. data/app/views/hr_lite/admin/roles/index.html.erb +1 -1
  47. data/app/views/hr_lite/admin/statutory_rate_cards/_form.html.erb +11 -7
  48. data/app/views/hr_lite/admin/tax_declarations/show.html.erb +6 -4
  49. data/app/views/hr_lite/admin/wfh_requests/show.html.erb +1 -1
  50. data/app/views/hr_lite/approvals/index.html.erb +4 -3
  51. data/app/views/hr_lite/attendance/_custom_status.html.erb +7 -9
  52. data/app/views/hr_lite/attendance/_punch_card.html.erb +8 -0
  53. data/app/views/hr_lite/attendance/show.html.erb +5 -3
  54. data/app/views/hr_lite/home/index.html.erb +4 -3
  55. data/app/views/hr_lite/leave_requests/_balance_chips.html.erb +2 -1
  56. data/app/views/hr_lite/loans/index.html.erb +1 -1
  57. data/app/views/hr_lite/resignations/show.html.erb +2 -1
  58. data/app/views/hr_lite/salary_slips/_slip_detail.html.erb +5 -5
  59. data/app/views/hr_lite/salary_slips/pdf.html.erb +1 -0
  60. data/app/views/hr_lite/work_time/_bars.html.erb +4 -3
  61. data/app/views/layouts/hr_lite/mailer.html.erb +10 -2
  62. data/app/views/layouts/hr_lite/mailer.text.erb +4 -1
  63. data/config/routes.rb +1 -1
  64. data/lib/hr_lite/amount_in_words.rb +7 -5
  65. data/lib/hr_lite/statutory_seeds.rb +1 -1
  66. data/lib/hr_lite/version.rb +1 -1
  67. metadata +1 -1
@@ -3,50 +3,29 @@
3
3
  <h1 class="hrl-page__title">HR overview</h1>
4
4
  </div>
5
5
 
6
+ <%# The Inbox badge's count and rows, so the board never counts what the Inbox hides. %>
7
+ <% pending = hrl_inbox_count %>
6
8
  <div class="hrl-kpis">
7
- <div class="hrl-kpi"><div class="hrl-kpi__value"><%= @kpis[:pending] %></div><div class="hrl-kpi__label">Pending approvals</div></div>
9
+ <div class="hrl-kpi"><div class="hrl-kpi__value"><%= pending %></div><div class="hrl-kpi__label">Pending approvals</div></div>
8
10
  <div class="hrl-kpi"><div class="hrl-kpi__value"><%= @kpis[:on_leave] %></div><div class="hrl-kpi__label">On leave today</div></div>
9
11
  <div class="hrl-kpi"><div class="hrl-kpi__value"><%= @kpis[:flagged] %></div><div class="hrl-kpi__label">Flagged punches</div></div>
10
12
  <div class="hrl-kpi"><div class="hrl-kpi__value"><%= @kpis[:missing_checkout] %></div><div class="hrl-kpi__label">Missing checkouts</div></div>
11
13
  </div>
12
14
 
13
- <% if @query.empty? %>
15
+ <% if pending.zero? && @kpis.except(:pending).values.all?(&:zero?) %>
14
16
  <p class="hrl-card hrl-card--empty">All present and accounted for.</p>
15
17
  <% else %>
16
- <% if @kpis[:pending].positive? %>
18
+ <% if pending.positive? %>
17
19
  <section class="hrl-card">
18
20
  <h2 class="hrl-card__title">Pending approvals</h2>
19
- <% @query.pending_requests.limit(HrLite::Admin::OverviewController::SECTION_CAP).each do |request| %>
21
+ <% hrl_inbox_rows(HrLite::Admin::OverviewController::SECTION_CAP).each do |tab, request| %>
22
+ <% what, href = hrl_inbox_item(tab, request) %>
20
23
  <div class="hrl-feed__item">
21
- <div class="hrl-feed__body">
22
- <strong><%= hr_display_name(request.user) %></strong>
23
- — <%= request.leave_type.name %>, <%= request.date_range_label %> (<%= hrl_days(request.days_count) %> d)
24
- </div>
25
- <a class="hrl-btn" href="<%= hr_lite.admin_leave_request_path(request) %>">Decide</a>
26
- </div>
27
- <% end %>
28
- <%# Comp-off and regularization are approvals too. Listing leave alone
29
- left both queues invisible on the board that exists to surface work. %>
30
- <% @query.pending_comp_offs.limit(HrLite::Admin::OverviewController::SECTION_CAP).each do |request| %>
31
- <div class="hrl-feed__item">
32
- <div class="hrl-feed__body">
33
- <strong><%= hr_display_name(request.user) %></strong>
34
- — comp-off for <%= request.date_worked.strftime("%d %b") %>
35
- (<%= hrl_days(request.credit_days) %> d)
36
- </div>
37
- <a class="hrl-btn" href="<%= hr_lite.admin_comp_off_request_path(request) %>">Decide</a>
38
- </div>
39
- <% end %>
40
- <% @query.pending_regularizations.limit(HrLite::Admin::OverviewController::SECTION_CAP).each do |request| %>
41
- <div class="hrl-feed__item">
42
- <div class="hrl-feed__body">
43
- <strong><%= hr_display_name(request.user) %></strong>
44
- — attendance fix for <%= request.date.strftime("%d %b") %>
45
- </div>
46
- <a class="hrl-btn" href="<%= hr_lite.admin_regularization_request_path(request) %>">Decide</a>
24
+ <div class="hrl-feed__body"><strong><%= hr_display_name(request.user) %></strong> — <%= what %></div>
25
+ <a class="hrl-btn" href="<%= href %>">Decide</a>
47
26
  </div>
48
27
  <% end %>
49
- <a class="hrl-card__link" href="<%= hr_lite.admin_leave_requests_path %>">All requests</a>
28
+ <a class="hrl-card__link" href="<%= hr_lite.approvals_path %>">All requests</a>
50
29
  </section>
51
30
  <% end %>
52
31
 
@@ -13,7 +13,8 @@
13
13
  <div class="hrl-field">
14
14
  <%= f.label :user_id, "Employee" %>
15
15
  <%= f.select :user_id, options_for_select(HrLite.employees.map { |u| [ hr_display_name(u), u.id ] },
16
- @item.user_id) %>
16
+ @item.user_id),
17
+ { include_blank: "Choose…" }, required: true, data: { hrl_filter: "Type a name to filter" } %>
17
18
  </div>
18
19
  <div class="hrl-field">
19
20
  <%= f.label :component_id, "Item" %>
@@ -2,7 +2,7 @@
2
2
  <div class="hrl-page__head">
3
3
  <h1 class="hrl-page__title">Payroll runs</h1>
4
4
  <div class="hrl-page__actions">
5
- <a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_admin_payroll_run_path %>">New run</a>
5
+ <% if hr_can?("payroll.manage", scope: :all) %><a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_admin_payroll_run_path %>">New run</a><% end %>
6
6
  </div>
7
7
  </div>
8
8
 
@@ -7,11 +7,11 @@
7
7
  </div>
8
8
  </div>
9
9
 
10
- <div class="hrl-kpis">
10
+ <div class="hrl-kpis hrl-kpis--money">
11
11
  <div class="hrl-kpi"><div class="hrl-kpi__value"><%= @slips.size %></div><div class="hrl-kpi__label">Slips</div></div>
12
12
  <div class="hrl-kpi"><div class="hrl-kpi__value"><%= hrl_money(@run.total_gross) %></div><div class="hrl-kpi__label">Gross</div></div>
13
13
  <div class="hrl-kpi"><div class="hrl-kpi__value"><%= hrl_money(@run.total_net) %></div><div class="hrl-kpi__label">Net payout</div></div>
14
- <div class="hrl-kpi"><div class="hrl-kpi__value"><%= hrl_money(@run.total_employer_cost) %></div><div class="hrl-kpi__label">Employer cost</div></div>
14
+ <div class="hrl-kpi"><div class="hrl-kpi__value"><%= hrl_money(@run.total_employer_cost) %></div><div class="hrl-kpi__label">Employer contributions</div></div>
15
15
  </div>
16
16
 
17
17
  <% if @run.warnings.any? %>
@@ -29,12 +29,14 @@
29
29
  <% manager = hr_can?("payroll.manage", scope: :all) %>
30
30
  <% if manager && (@run.draft? || @run.review? || @run.processing?) %>
31
31
  <%= button_to @run.draft? ? "Compute" : "Recompute", hr_lite.compute_admin_payroll_run_path(@run),
32
- method: :post, class: "hrl-btn" %>
32
+ method: :post, class: "hrl-btn",
33
+ form: { data: { turbo_confirm: ("Recompute #{@run.label}? This clears #{hr_display_name(@run.first_approved_by)}'s approval." if @run.first_approved_by) } } %>
33
34
  <% end %>
34
35
  <% if @run.review? && hr_can?("payroll.approve", scope: :all) && @run.first_approved_by_id != hr_current_user.id %>
35
36
  <%= button_to @run.first_approved_by_id ? "Approve and publish" : "Approve", hr_lite.approve_admin_payroll_run_path(@run),
36
37
  method: :post, class: "hrl-btn hrl-btn--primary",
37
- form: { data: { turbo_confirm: @run.first_approved_by_id ? "Approve #{@run.label}? This finalizes the run and publishes the slips." : "Approve #{@run.label}?" } } %>
38
+ form: { data: { turbo_confirm: [ @run.first_approved_by_id ? "Approve #{@run.label}? This finalizes the run and publishes the slips." : "Approve #{@run.label}?",
39
+ *("Blocking warnings: #{@run.blocking_warnings.join('; ')}" if @run.blocking_warnings.any?) ].join("\n\n") } } %>
38
40
  <% end %>
39
41
  <% if manager && @run.finalized? %>
40
42
  <%= button_to "Unlock", hr_lite.unlock_admin_payroll_run_path(@run), method: :post, class: "hrl-btn" %>
@@ -22,7 +22,8 @@
22
22
  <div class="hrl-field"><%= f.label :outcome_note, "Outcome note" %><%= f.text_area :outcome_note, rows: 3 %></div>
23
23
  <% end %>
24
24
  <div class="hrl-form-actions">
25
- <%= f.submit plan.persisted? ? "Save" : "Start PIP", class: "hrl-btn hrl-btn--primary" %>
25
+ <%= f.submit plan.persisted? ? "Save" : "Start PIP", class: "hrl-btn hrl-btn--primary",
26
+ data: { turbo_confirm: ("Save? Passed or failed closes the plan for good and tells the employee." if plan.persisted?) } %>
26
27
  <a class="hrl-btn" href="<%= hr_lite.admin_performance_plans_path %>">Cancel</a>
27
28
  </div>
28
29
  <% end %>
@@ -14,7 +14,7 @@
14
14
  <tbody>
15
15
  <% @policies.each do |policy| %>
16
16
  <tr>
17
- <td data-label="Title"><%= policy.title %> <span class="hrl-badge hrl-badge--muted">v<%= policy.version %></span></td>
17
+ <td data-label="Title"><a class="hrl-card__link" href="<%= hr_lite.policy_path(policy) %>"><%= policy.title %></a> <span class="hrl-badge hrl-badge--muted">v<%= policy.version %></span></td>
18
18
  <td data-label="Kind"><%= policy.announcement? ? "Announcement" : "Policy" %></td>
19
19
  <td data-label="In force"><%= policy.effective_from.strftime("%d %b %Y") %></td>
20
20
  <td data-label="Still to acknowledge"><%= policy.announcement? ? "—" : policy.outstanding_for.count %></td>
@@ -15,7 +15,8 @@
15
15
  <%= f.label :acknowledgement_required, "Policy — everyone must acknowledge it (untick for an announcement)" %>
16
16
  </div>
17
17
  <div class="hrl-form-actions">
18
- <%= f.submit "Publish", class: "hrl-btn hrl-btn--primary" %>
18
+ <%= f.submit "Publish", class: "hrl-btn hrl-btn--primary",
19
+ data: { turbo_confirm: "Publish and notify #{pluralize(HrLite.active_employees.size, 'person')} by email?" } %>
19
20
  <a class="hrl-btn" href="<%= hr_lite.admin_policies_path %>">Cancel</a>
20
21
  </div>
21
22
  <% end %>
@@ -54,7 +54,7 @@
54
54
  <%= form_with url: hr_lite.reject_admin_regularization_request_path(@request), method: :post, local: true do |f| %>
55
55
  <div class="hrl-field">
56
56
  <%= f.label :decision_note, "Reason (required)", for: "reject_note" %>
57
- <%= f.text_area :decision_note, id: "reject_note" %>
57
+ <%= f.text_area :decision_note, id: "reject_note", required: true %>
58
58
  </div>
59
59
  <div class="hrl-form-actions">
60
60
  <%= f.button "Reject", type: :submit, class: "hrl-btn hrl-btn--danger" %>
@@ -40,7 +40,8 @@
40
40
  <% end %>
41
41
 
42
42
  <div class="hrl-form-actions">
43
- <%= f.submit "Save", class: "hrl-btn hrl-btn--primary" %>
43
+ <%= f.submit "Save", class: "hrl-btn hrl-btn--primary",
44
+ data: { turbo_confirm: ("Save? Everyone holding #{role.name} gets these permissions at once." if role.persisted?) } %>
44
45
  <a class="hrl-btn" href="<%= hr_lite.admin_roles_path %>">Cancel</a>
45
46
  </div>
46
47
  <% end %>
@@ -38,13 +38,15 @@
38
38
  </div>
39
39
  <% end %>
40
40
 
41
- <%= form_with url: hr_lite.admin_role_assignments_path(@role), method: :post, local: true do %>
41
+ <%= form_with url: hr_lite.admin_role_assignments_path(@role), method: :post, local: true,
42
+ data: { turbo_confirm: "Give the #{@role.name} role to the person you picked? Their access changes at once." } do %>
42
43
  <div class="hrl-field">
43
44
  <%= label_tag :user_id, "Add somebody to this role" %>
44
45
  <% held = @role.role_assignments.map(&:user_id) %>
45
46
  <%= select_tag :user_id,
46
47
  options_for_select(HrLite.employees.reject { |u| held.include?(u.id) }
47
- .map { |u| [ hr_display_name(u), u.id ] }) %>
48
+ .map { |u| [ hr_display_name(u), u.id ] }),
49
+ include_blank: "Choose…", required: true, data: { hrl_filter: "Type a name to filter" } %>
48
50
  </div>
49
51
  <div class="hrl-form-actions">
50
52
  <%= submit_tag "Add", class: "hrl-btn hrl-btn--primary" %>
@@ -39,7 +39,7 @@
39
39
  <% end %>
40
40
  </td>
41
41
  <td data-label="">
42
- <a class="hrl-small" href="<%= hr_lite.edit_admin_role_path(role) %>">Edit</a>
42
+ <a class="hrl-btn" href="<%= hr_lite.edit_admin_role_path(role) %>">Edit</a>
43
43
  </td>
44
44
  </tr>
45
45
  <% end %>
@@ -68,16 +68,20 @@
68
68
  the open-ended top rate.
69
69
  </p>
70
70
  <% slabs = Array(table["slabs"]) %>
71
- <% (slabs + [ nil, nil ]).each_with_index do |row, index| %>
72
- <div class="hrl-row">
71
+ <% num = ->(v) { v && number_with_precision(v, precision: 4, strip_insignificant_zeros: true) } %>
72
+ <div class="hrl-slabs">
73
+ <span class="hrl-small" aria-hidden="true">From ₹</span>
74
+ <span class="hrl-small" aria-hidden="true">Up to ₹</span>
75
+ <span class="hrl-small" aria-hidden="true">Rate (0.05 = 5%)</span>
76
+ <% (slabs + [ nil, nil ]).each_with_index do |row, index| %>
73
77
  <%= text_field_tag "statutory_rate_card[income_tax][#{regime}][slabs][#{index}][from]",
74
- row&.first, placeholder: "From ₹", inputmode: "decimal" %>
78
+ num.(row&.first), class: "hrl-input", inputmode: "decimal", "aria-label": "Band #{index + 1} from ₹" %>
75
79
  <%= text_field_tag "statutory_rate_card[income_tax][#{regime}][slabs][#{index}][to]",
76
- row&.at(1), placeholder: "Up to ₹ (blank = top)", inputmode: "decimal" %>
80
+ num.(row&.at(1)), class: "hrl-input", inputmode: "decimal", "aria-label": "Band #{index + 1} up to ₹ (blank = top)" %>
77
81
  <%= text_field_tag "statutory_rate_card[income_tax][#{regime}][slabs][#{index}][rate]",
78
- row&.last, placeholder: "Rate (0.05)", inputmode: "decimal" %>
79
- </div>
80
- <% end %>
82
+ num.(row&.last), class: "hrl-input", inputmode: "decimal", "aria-label": "Band #{index + 1} rate" %>
83
+ <% end %>
84
+ </div>
81
85
  </fieldset>
82
86
  <% end %>
83
87
 
@@ -48,6 +48,7 @@
48
48
  <% end %>
49
49
  <div class="hrl-form-actions">
50
50
  <%= f.submit "Record amounts", class: "hrl-btn" %>
51
+ <%= f.submit "Record and verify", name: "verify", class: "hrl-btn" %>
51
52
  </div>
52
53
  </section>
53
54
  <% end %>
@@ -55,16 +56,17 @@
55
56
  <section class="hrl-card">
56
57
  <h2 class="hrl-card__title">Decide</h2>
57
58
  <div class="hrl-row">
59
+ <% unproven = @declaration.tax_declaration_items.reject { |item| item.proofs.attached? }.map { |item| item.section_label.split(" — ").first } %>
58
60
  <%= button_to "Accept all and verify", hr_lite.accept_all_admin_tax_declaration_path(@declaration),
59
- method: :post, class: "hrl-btn hrl-btn--primary" %>
60
- <%= button_to "Verify the amounts recorded above", hr_lite.verify_admin_tax_declaration_path(@declaration),
61
- method: :post, class: "hrl-btn" %>
61
+ method: :post, class: "hrl-btn hrl-btn--primary",
62
+ form: { data: { turbo_confirm: [ "Accept every claimed amount and verify? The employee can no longer edit it.",
63
+ ("No proof attached: #{unproven.to_sentence}." if unproven.any?) ].compact.join(" ") } } %>
62
64
  </div>
63
65
  <%= form_with url: hr_lite.reject_admin_tax_declaration_path(@declaration),
64
66
  method: :post, local: true do %>
65
67
  <div class="hrl-field">
66
68
  <%= label_tag :note, "What is missing (required to send back)" %>
67
- <%= text_field_tag :note %>
69
+ <%= text_field_tag :note, nil, class: "hrl-input" %>
68
70
  </div>
69
71
  <%= submit_tag "Send back", class: "hrl-btn" %>
70
72
  <% end %>
@@ -33,7 +33,7 @@
33
33
  <section class="hrl-card">
34
34
  <h2 class="hrl-card__title">Reject</h2>
35
35
  <%= form_with url: hr_lite.reject_admin_wfh_request_path(@request), method: :post, local: true do |f| %>
36
- <div class="hrl-field"><%= f.label :decision_note, "Remark (required)", for: "reject_note" %><%= f.text_area :decision_note, id: "reject_note" %></div>
36
+ <div class="hrl-field"><%= f.label :decision_note, "Remark (required)", for: "reject_note" %><%= f.text_area :decision_note, id: "reject_note", required: true %></div>
37
37
  <div class="hrl-form-actions"><%= f.button "Reject", type: :submit, class: "hrl-btn hrl-btn--danger" %></div>
38
38
  <% end %>
39
39
  </section>
@@ -45,13 +45,14 @@
45
45
  </td>
46
46
  </tr>
47
47
  <% end %>
48
- <%# The HR queues this viewer decides; Open lands on that queue's own page. %>
48
+ <%# The HR queues this viewer decides; Open lands on the request itself where it has a page. %>
49
49
  <% rows.each do |tab, record| %>
50
+ <% what, href = hrl_inbox_item(tab, record) %>
50
51
  <tr>
51
- <td data-label="Request"><strong><%= tab[:label] %></strong></td>
52
+ <td data-label="Request"><strong><%= what %></strong></td>
52
53
  <td data-label="From"><%= hr_display_name(record.user) %></td>
53
54
  <td data-label="Waiting"><%= time_ago_in_words(record.created_at) %></td>
54
- <td data-label=""><a class="hrl-btn" href="<%= hr_lite.public_send(tab[:path]) %>">Open</a></td>
55
+ <td data-label=""><a class="hrl-btn" href="<%= href %>">Open</a></td>
55
56
  </tr>
56
57
  <% end %>
57
58
  </tbody>
@@ -1,6 +1,6 @@
1
- <%# The person's own status (text + emoji), Slack-style. Independent of check-in. %>
1
+ <%# The person's own status, Slack-style. Independent of check-in. Quick picks are words: no emoji in the UI. %>
2
2
  <% custom = status.custom
3
- picks = %w[🏠 🏢 ✈️ 🚗 🚆 🏖️ 🤒 🤧 🩺 🍽️ ☕ 🎧 🎯 💻 📞 📅 🗓️ 🤝 💼 📍 🧳 🏔️ 🌴 🏕️ 📝 📊 🔒 🚫 ⏳ 🔕 🎉 🙏 👶 👨‍👩‍👧 🏥 📚 🎤 🛠️ 🌙 🌧️] %>
3
+ picks = [ "In a meeting", "On a call", "Focus time", "At lunch", "Commuting", "Travelling", "At a client", "Out sick" ] %>
4
4
  <% if custom %>
5
5
  <div class="hrl-row hrl-row--mb hrl-small">
6
6
  <span class="hrl-custom-status"><% if custom.emoji %><span aria-hidden="true"><%= custom.emoji %></span> <% end %><%= custom.title || "Status" %></span>
@@ -10,15 +10,13 @@
10
10
  <% end %>
11
11
  <details>
12
12
  <summary class="hrl-btn hrl-btn--block"><%= custom ? "Change my status" : "Set a status" %></summary>
13
- <%= form_with url: hr_lite.status_work_mode_path, method: :post, local: true, data: { "hrl-emoji-form": true } do %>
14
- <div class="hrl-field"><label for="st-emoji">Emoji</label>
15
- <input class="hrl-input" id="st-emoji" name="emoji" maxlength="16" value="<%= custom&.emoji %>" placeholder="Pick or type one" autocomplete="off">
16
- <div class="hrl-emoji-grid" role="group" aria-label="Pick an emoji">
17
- <% picks.each do |e| %><button type="button" class="hrl-emoji" data-hrl-emoji="<%= e %>" aria-label="Use <%= e %>"><%= e %></button><% end %>
13
+ <%= form_with url: hr_lite.status_work_mode_path, method: :post, local: true do %>
14
+ <div class="hrl-field"><label for="st-text">What's your status?</label>
15
+ <input class="hrl-input" id="st-text" name="text" maxlength="100" value="<%= custom&.title %>" placeholder="e.g. Visiting the Ladakh camp">
16
+ <div class="hrl-row hrl-mt" role="group" aria-label="Quick picks">
17
+ <% picks.each do |text| %><button type="button" class="hrl-btn" data-hrl-pick="<%= text %>"><%= text %></button><% end %>
18
18
  </div>
19
19
  </div>
20
- <div class="hrl-field"><label for="st-text">What's your status?</label>
21
- <input class="hrl-input" id="st-text" name="text" maxlength="100" value="<%= custom&.title %>" placeholder="e.g. Visiting the Ladakh camp"></div>
22
20
  <div class="hrl-field"><label for="st-clear">Clear after</label>
23
21
  <%= select_tag :clear_after, options_for_select([ [ "30 minutes", "30" ], [ "1 hour", "60" ], [ "4 hours", "240" ], [ "Today", "today" ], [ "Don't clear", "never" ] ], "today"), id: "st-clear" %></div>
24
22
  <button type="submit" class="hrl-btn hrl-btn--primary">Save status</button>
@@ -93,6 +93,14 @@
93
93
  </fieldset></div>
94
94
  <% end %>
95
95
  <button type="submit" class="hrl-btn hrl-btn--primary hrl-btn--big hrl-btn--block"><%= record&.check_out_at ? "Check in again" : "Check in" %></button>
96
+ <% if HrLite.config.attendance_slots && !record&.check_out_at %>
97
+ <%# The late rules AttendancePuncher#apply_slot_rules applies, shifted by the person's buffer like there. %>
98
+ <% shift = HrLite::EmployeeProfile.where(user_id: hr_current_user.id).pick(:buffer_minutes).to_i
99
+ by = HrLite::AttendanceSlot.start_minute_for(hr_current_user.id, Date.current) + HrLite::AttendanceSlot::GRACE_MINUTES + shift
100
+ at = ->(minute) { HrLite::AttendanceSlot.label(minute + shift) } %>
101
+ <p class="hrl-small hrl-mt">Check in by <strong><%= HrLite::AttendanceSlot.label(by) %></strong>. Later adds 30 min to your day for every half hour;
102
+ after <%= at.(HrLite::AttendanceSlot::CARRY_AFTER_MINUTE) %> also 1 hour on each of the next 2 working days; after <%= at.(HrLite::AttendanceSlot::HALF_DAY_AFTER_MINUTE) %> the day is a half day.</p>
103
+ <% end %>
96
104
  <p class="hrl-small hrl-muted" data-hrl-geo-status role="status" aria-live="polite"></p>
97
105
  <% end %>
98
106
  <% elsif record.check_out_at.nil? %>
@@ -20,9 +20,11 @@
20
20
  </div>
21
21
  <%= render "hr_lite/attendance/month_grid", month: @month, day_status: @day_status, pending_dates: @pending_dates,
22
22
  link_builder: ->(d) { hr_lite.attendance_path(month: @month.strftime("%Y-%m"), day: d.iso8601, anchor: "day") if d <= Date.current } %>
23
- <p class="hrl-small hrl-muted hrl-mt">Forgot to check in or out on some day?
24
- <a href="<%= hr_lite.new_regularization_request_path %>">Raise a regularization ticket</a>
25
- · <a href="<%= hr_lite.regularization_requests_path %>">My tickets</a></p>
23
+ <p class="hrl-small hrl-muted hrl-mt">Forgot to check in or out on some day?</p>
24
+ <div class="hrl-row">
25
+ <a class="hrl-btn" href="<%= hr_lite.new_regularization_request_path %>">Raise a regularization ticket</a>
26
+ <a class="hrl-btn" href="<%= hr_lite.regularization_requests_path %>">My tickets</a>
27
+ </div>
26
28
  </section>
27
29
 
28
30
  <% if HrLite.config.attendance_slots %>
@@ -2,10 +2,11 @@
2
2
  <h1 class="hrl-page__title">Hello, <%= hr_display_name(hr_current_user) %></h1>
3
3
  </div>
4
4
 
5
- <% if (@waiting + @queue).positive? %>
5
+ <% waiting = @waiting + hrl_inbox_count %>
6
+ <% if waiting.positive? %>
6
7
  <section class="hrl-card">
7
- <h2 class="hrl-card__title"><%= pluralize(@waiting + @queue, "request") %> waiting on you</h2>
8
- <a class="hrl-btn hrl-btn--primary hrl-mt" href="<%= @waiting.positive? ? hr_lite.approvals_path : hr_lite.admin_leave_requests_path %>">Review requests</a>
8
+ <h2 class="hrl-card__title"><%= pluralize(waiting, "request") %> waiting on you</h2>
9
+ <a class="hrl-btn hrl-btn--primary hrl-mt" href="<%= hr_lite.approvals_path %>">Review requests</a>
9
10
  </section>
10
11
  <% end %>
11
12
 
@@ -7,8 +7,9 @@
7
7
  <div class="hrl-row">
8
8
  <% types.each do |type| %>
9
9
  <% balance = HrLite::LeaveBalance.for(user, type, HrLite::LeaveYear.current_key) %>
10
+ <% pending = HrLite::LeaveRequest.pending.where(user_id: user.id, leave_type_id: type.id, start_date: HrLite::LeaveYear.range(HrLite::LeaveYear.current_key)).sum(&:days_count) %>
10
11
  <span class="hrl-badge hrl-badge--type" style="--hrl-type: <%= type.color %>">
11
- <%= type.name %> · <%= hrl_days(balance.available) %> of <%= hrl_days(balance.entitled) %> left<% if (held = balance.penalty_days).positive? %> · <%= hrl_days(held) %> held for penalties<% end %>
12
+ <%= type.name %> · <%= hrl_days(balance.available) %> of <%= hrl_days(balance.entitled) %> left<% if pending.positive? %> · <%= hrl_days(pending) %> pending<% end %><% if (held = balance.penalty_days).positive? %> · <%= hrl_days(held) %> held for penalties<% end %>
12
13
  </span>
13
14
  <% end %>
14
15
  </div>
@@ -21,7 +21,7 @@
21
21
  <dl class="hrl-deflist">
22
22
  <dt>Monthly instalment</dt><dd><%= hrl_money(loan.monthly_instalment) %></dd>
23
23
  <dt>Repaid so far</dt><dd><%= hrl_money(loan.repaid) %></dd>
24
- <dt>Outstanding</dt><dd><strong><%= hrl_money(loan.outstanding) %></strong></dd>
24
+ <dt>Outstanding</dt><dd><strong><%= loan.active? ? hrl_money(loan.outstanding) : "—" %></strong></dd>
25
25
  <dt>Next deduction</dt>
26
26
  <dd>
27
27
  <% amount = loan.instalment_for(@next_month) %>
@@ -35,7 +35,8 @@
35
35
  <%= f.text_area :reason %>
36
36
  </div>
37
37
  <div class="hrl-form-actions">
38
- <%= f.submit "Submit resignation", class: "hrl-btn hrl-btn--primary" %>
38
+ <%= f.submit "Submit resignation", class: "hrl-btn hrl-btn--primary",
39
+ data: { turbo_confirm: "Submit your resignation? Leadership is told straight away." } %>
39
40
  </div>
40
41
  <% end %>
41
42
  </section>
@@ -9,17 +9,17 @@
9
9
  <section class="hrl-card">
10
10
  <h2 class="hrl-card__title">Earnings</h2>
11
11
  <div class="hrl-table-wrap">
12
- <table class="hrl-table">
12
+ <table class="hrl-table hrl-table--stack">
13
13
  <thead><tr><th>Component</th><th class="hrl-num">Full</th><th class="hrl-num">Earned</th></tr></thead>
14
14
  <tbody>
15
15
  <% slip.earnings_rows.each do |row| %>
16
16
  <tr>
17
- <td><%= row["label"] %></td>
18
- <td class="hrl-num"><%= hrl_money(row["full_amount"]) %></td>
19
- <td class="hrl-num"><%= hrl_money(row["amount"]) %></td>
17
+ <td data-label="Component"><%= row["label"] %></td>
18
+ <td data-label="Full" class="hrl-num"><%= hrl_money(row["full_amount"]) %></td>
19
+ <td data-label="Earned" class="hrl-num"><%= hrl_money(row["amount"]) %></td>
20
20
  </tr>
21
21
  <% end %>
22
- <tr><td><strong>Gross</strong></td><td></td><td class="hrl-num"><strong><%= hrl_money(slip.gross_earnings) %></strong></td></tr>
22
+ <tr><td data-label="Component"><strong>Gross</strong></td><td class="hrl-cell--void"></td><td data-label="Earned" class="hrl-num"><strong><%= hrl_money(slip.gross_earnings) %></strong></td></tr>
23
23
  </tbody>
24
24
  </table>
25
25
  </div>
@@ -7,6 +7,7 @@
7
7
  <% end %>
8
8
  <div class="pdf-company"><%= company[:name] %></div>
9
9
  <% if company[:address].present? %><div class="pdf-muted"><%= company[:address] %></div><% end %>
10
+ <% if (contact = company.values_at(:phone, :email, :website).compact_blank).any? %><div class="pdf-muted"><%= contact.join(" · ") %></div><% end %>
10
11
  </div>
11
12
  <div style="text-align:right;">
12
13
  <h1>Salary slip</h1>
@@ -7,11 +7,12 @@
7
7
  <section class="hrl-card">
8
8
  <h2 class="hrl-card__title">By <%= period.bucket %></h2>
9
9
  <div class="hrl-chart-scroll">
10
- <svg class="hrl-bars" viewBox="0 0 <%= buckets.size * (w + gap) %> <%= h + 14 %>" role="img"
10
+ <svg class="hrl-bars" style="--hrl-bars-n: <%= buckets.size %>" viewBox="0 0 <%= buckets.size * (w + gap) %> <%= h + 14 %>" role="img"
11
11
  aria-label="Time per <%= period.bucket %> stacked by work mode; tallest bar <%= hrl_duration(max) %>; details in the table">
12
12
  <% buckets.each_with_index do |(key, modes), i| %>
13
13
  <% x = i * (w + gap); y = h %>
14
- <a href="<%= path.(period: period.bucket == :day ? "day" : (period.bucket == :week ? "week" : "month"), date: key) %>">
14
+ <a href="<%= path.(period: period.bucket == :day ? "day" : (period.bucket == :week ? "week" : "month"), date: key) %>"
15
+ aria-label="<%= key.strftime(period.bucket == :day ? "%a %-d %b" : fmt) %>: <%= hrl_duration(totals[key]) %>">
15
16
  <% modes.except("offline").sort.each do |mode, secs| %>
16
17
  <% next unless secs.positive? %>
17
18
  <% bh = secs / max * h; y -= bh %>
@@ -28,7 +29,7 @@
28
29
  </svg>
29
30
  </div>
30
31
  <details class="hrl-small">
31
- <summary>Show as a table</summary>
32
+ <summary class="hrl-card__link">Show as a table</summary>
32
33
  <table class="hrl-table">
33
34
  <thead><tr><th><%= period.bucket.to_s.capitalize %></th><th class="hrl-num">Working (breaks incl.)</th><th class="hrl-num">Of which breaks</th><th class="hrl-num">Total</th></tr></thead>
34
35
  <tbody>
@@ -21,9 +21,17 @@
21
21
  <body>
22
22
  <div class="wrap">
23
23
  <div class="card">
24
- <div class="brand"><%= HrLite.config.company.call[:name].presence || "HR" %></div>
24
+ <% company = HrLite.config.company.call %>
25
+ <%# Mail clients strip data: URIs, so this is the host's absolute https logo, not logo_url. %>
26
+ <% if company[:email_logo_url].present? %>
27
+ <img src="<%= company[:email_logo_url] %>" alt="<%= company[:name] %>" height="40" style="display:block; height:40px; width:auto; border:0; margin-bottom:12px;">
28
+ <% end %>
29
+ <div class="brand"><%= company[:name].presence || "HR" %></div>
25
30
  <%= yield %>
26
- <p class="foot">Sent by the HR system. Do not reply to this email.</p>
31
+ <p class="foot">
32
+ Sent by the HR system of <%= company[:name].presence || "your company" %>. Do not reply to this email.
33
+ <% if (contact = company.values_at(:phone, :email, :website).compact_blank).any? %><br>Questions: <%= contact.join(" · ") %><% end %>
34
+ </p>
27
35
  </div>
28
36
  </div>
29
37
  </body>
@@ -1,6 +1,9 @@
1
- <%= HrLite.config.company.call[:name].presence || "HR" %>
1
+ <% company = HrLite.config.company.call %>
2
+ <%= company[:name].presence || "HR" %>
2
3
 
3
4
  <%= yield %>
4
5
 
5
6
  --
6
7
  Sent by the HR system. Do not reply to this email.
8
+ <% if (contact = company.values_at(:phone, :email, :website).compact_blank).any? %>Questions: <%= contact.join(" · ") %>
9
+ <% end %>
data/config/routes.rb CHANGED
@@ -115,7 +115,7 @@ HrLite::Engine.routes.draw do
115
115
  end
116
116
  resources :reports, only: %i[index show]
117
117
  resources :expenses, only: :index do
118
- member { post :approve; post :reject; post :reimburse }
118
+ member { post :approve; post :reject; post :reimburse; get :receipt }
119
119
  end
120
120
  resources :hr_requests, only: %i[index show] do
121
121
  member { post :assign; post :resolve }
@@ -10,15 +10,17 @@ module HrLite
10
10
 
11
11
  module_function
12
12
 
13
+ # Paise are spelled too: a slip showing ₹99,612.91 must not say "six hundred thirteen".
13
14
  def words(amount)
14
- rupees = Money.round_rupee(amount).to_i
15
- return "Zero rupees" if rupees.zero?
15
+ paise = (Money.d(amount) * 100).round(0, BigDecimal::ROUND_HALF_UP).to_i
16
+ return "Zero rupees" if paise.zero?
16
17
 
18
+ rupees, cents = paise.abs.divmod(100)
17
19
  # A negative amount rendered as bare " rupees": no group was >= its
18
20
  # divisor, so nothing was ever appended.
19
- sentence = groups(rupees.abs).join(" ")
20
- sentence = "minus #{sentence}" if rupees.negative?
21
- "#{sentence.capitalize} rupees"
21
+ sentence = rupees.zero? ? "zero" : groups(rupees).join(" ")
22
+ sentence = "minus #{sentence}" if paise.negative?
23
+ "#{sentence.capitalize} rupees#{" and #{two_digit(cents)} paise" if cents.positive?}"
22
24
  end
23
25
 
24
26
  def groups(rupees)
@@ -22,7 +22,7 @@ module HrLite
22
22
  income_tax: card[:income_tax].transform_values { |regime| stringify_regime(regime) },
23
23
  notes: StatutoryRateCard::SOURCES[effective_from] ||
24
24
  "Shipped with hr_lite #{HrLite::VERSION}. Confirm with your accountant, then record who verified it.",
25
- verified_by: (StatutoryRateCard::SOURCES.key?(effective_from) ? "hr_lite #{HrLite::VERSION} (official source in the notes)" : nil),
25
+ verified_by: (StatutoryRateCardRecord::SHIPPED_SIGN_OFF if StatutoryRateCard::SOURCES.key?(effective_from)),
26
26
  verified_on: (Date.current if StatutoryRateCard::SOURCES.key?(effective_from))
27
27
  )
28
28
  "rate card FY #{FinancialYear.label(effective_from)}"
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.53.0"
2
+ VERSION = "0.54.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.53.0
4
+ version: 0.54.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kshitiz sinha