hr_lite 0.52.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +5 -2
  4. data/app/assets/javascripts/hr_lite/emoji_pick.js +4 -4
  5. data/app/assets/javascripts/hr_lite/vendor/turbo.min.js +28 -0
  6. data/app/assets/stylesheets/hr_lite/hr_lite.css +7 -6
  7. data/app/controllers/hr_lite/admin/attendances_controller.rb +2 -0
  8. data/app/controllers/hr_lite/admin/comp_off_requests_controller.rb +1 -1
  9. data/app/controllers/hr_lite/admin/documents_controller.rb +1 -1
  10. data/app/controllers/hr_lite/admin/expenses_controller.rb +13 -1
  11. data/app/controllers/hr_lite/admin/leave_requests_controller.rb +1 -1
  12. data/app/controllers/hr_lite/admin/regularization_requests_controller.rb +1 -1
  13. data/app/controllers/hr_lite/admin/statutory_rate_cards_controller.rb +2 -1
  14. data/app/controllers/hr_lite/admin/tax_declarations_controller.rb +2 -1
  15. data/app/controllers/hr_lite/admin/work_locations_controller.rb +1 -1
  16. data/app/controllers/hr_lite/home_controller.rb +0 -9
  17. data/app/controllers/hr_lite/loans_controller.rb +1 -1
  18. data/app/helpers/hr_lite/application_helper.rb +21 -2
  19. data/app/models/hr_lite/attendance_record.rb +7 -4
  20. data/app/models/hr_lite/salary_slip.rb +2 -1
  21. data/app/models/hr_lite/statutory_rate_card_record.rb +6 -0
  22. data/app/views/hr_lite/admin/attendances/index.html.erb +4 -4
  23. data/app/views/hr_lite/admin/attendances/show.html.erb +1 -1
  24. data/app/views/hr_lite/admin/benefits/index.html.erb +10 -4
  25. data/app/views/hr_lite/admin/comp_off_requests/show.html.erb +1 -1
  26. data/app/views/hr_lite/admin/documents/index.html.erb +2 -2
  27. data/app/views/hr_lite/admin/employees/show.html.erb +2 -1
  28. data/app/views/hr_lite/admin/expense_categories/index.html.erb +1 -1
  29. data/app/views/hr_lite/admin/expenses/index.html.erb +4 -2
  30. data/app/views/hr_lite/admin/holidays/index.html.erb +2 -1
  31. data/app/views/hr_lite/admin/hr_requests/index.html.erb +2 -2
  32. data/app/views/hr_lite/admin/hr_requests/show.html.erb +1 -1
  33. data/app/views/hr_lite/admin/leave_requests/show.html.erb +1 -1
  34. data/app/views/hr_lite/admin/leave_types/index.html.erb +2 -2
  35. data/app/views/hr_lite/admin/loans/new.html.erb +1 -1
  36. data/app/views/hr_lite/admin/office_locations/index.html.erb +1 -1
  37. data/app/views/hr_lite/admin/overview/index.html.erb +10 -31
  38. data/app/views/hr_lite/admin/payroll_line_items/new.html.erb +2 -1
  39. data/app/views/hr_lite/admin/payroll_runs/index.html.erb +1 -1
  40. data/app/views/hr_lite/admin/payroll_runs/show.html.erb +6 -4
  41. data/app/views/hr_lite/admin/performance_plans/_form.html.erb +2 -1
  42. data/app/views/hr_lite/admin/policies/index.html.erb +1 -1
  43. data/app/views/hr_lite/admin/policies/new.html.erb +2 -1
  44. data/app/views/hr_lite/admin/regularization_requests/show.html.erb +1 -1
  45. data/app/views/hr_lite/admin/roles/_form.html.erb +2 -1
  46. data/app/views/hr_lite/admin/roles/edit.html.erb +4 -2
  47. data/app/views/hr_lite/admin/roles/index.html.erb +1 -1
  48. data/app/views/hr_lite/admin/statutory_rate_cards/_form.html.erb +11 -7
  49. data/app/views/hr_lite/admin/tax_declarations/show.html.erb +6 -4
  50. data/app/views/hr_lite/admin/wfh_requests/show.html.erb +1 -1
  51. data/app/views/hr_lite/approvals/index.html.erb +4 -3
  52. data/app/views/hr_lite/attendance/_custom_status.html.erb +7 -9
  53. data/app/views/hr_lite/attendance/_punch_card.html.erb +8 -0
  54. data/app/views/hr_lite/attendance/show.html.erb +5 -3
  55. data/app/views/hr_lite/home/index.html.erb +4 -3
  56. data/app/views/hr_lite/leave_requests/_balance_chips.html.erb +2 -1
  57. data/app/views/hr_lite/loans/index.html.erb +1 -1
  58. data/app/views/hr_lite/resignations/show.html.erb +2 -1
  59. data/app/views/hr_lite/salary_slips/_slip_detail.html.erb +5 -5
  60. data/app/views/hr_lite/salary_slips/pdf.html.erb +1 -0
  61. data/app/views/hr_lite/work_time/_bars.html.erb +4 -3
  62. data/app/views/layouts/hr_lite/application.html.erb +7 -0
  63. data/app/views/layouts/hr_lite/mailer.html.erb +10 -2
  64. data/app/views/layouts/hr_lite/mailer.text.erb +4 -1
  65. data/config/routes.rb +1 -1
  66. data/lib/hr_lite/amount_in_words.rb +7 -5
  67. data/lib/hr_lite/configuration.rb +4 -0
  68. data/lib/hr_lite/statutory_seeds.rb +1 -1
  69. data/lib/hr_lite/version.rb +1 -1
  70. metadata +2 -1
@@ -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>
@@ -7,6 +7,13 @@
7
7
  <%= csrf_meta_tags %>
8
8
  <%= csp_meta_tag %>
9
9
  <%= yield :head %>
10
+ <% if HrLite.config.native_app.call(request) %>
11
+ <%# The host's iOS/Android shells drive pages through Turbo. "reload" makes
12
+ every visit a full page load, so the plain scripts below run exactly
13
+ as they do in a browser. Browsers never load Turbo here. %>
14
+ <meta name="turbo-visit-control" content="reload">
15
+ <%= javascript_include_tag "hr_lite/vendor/turbo.min", type: "module" %>
16
+ <% end %>
10
17
  <%= stylesheet_link_tag "hr_lite/hr_lite", media: "all" %>
11
18
  <% HrLite.config.extra_stylesheets.each do |sheet| %>
12
19
  <%= stylesheet_link_tag sheet, media: "all" %>
@@ -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)
@@ -13,6 +13,9 @@ module HrLite
13
13
  # ->(user) { { url:, unread: } } — shows a notifications link in
14
14
  # the HR shell; nil (default) shows nothing.
15
15
  :notifications,
16
+ # ->(request) { bool } — is this request from the host's native app?
17
+ # Native pages load Turbo (see the layout); browsers never do.
18
+ :native_app,
16
19
  # e.g. 6 — default probation for new hires (admin edits per employee)
17
20
  :probation_months,
18
21
  # true = a punch without GPS is refused (default: recorded and flagged)
@@ -114,6 +117,7 @@ module HrLite
114
117
  @public_url_base = nil # e.g. "https://hr.example.com" — enables email links + HrLite.public_url
115
118
  @notification_matrix = nil # resolved lazily to Notifications::DEFAULT_MATRIX
116
119
  @back_link = nil # optional {label:, url:} for the shell nav
120
+ @native_app = ->(request) { request.user_agent.to_s.match?(/Hotwire Native|Turbo Native/) }
117
121
  @leave_year_start_month = 1 # 1 = calendar year; 7 = July–June leave year
118
122
  @slip_release_day = nil # e.g. 10 = slips open on the 10th of the next month
119
123
  @self_regularization = nil # e.g. { within_days: 2, per_week: 2 } — employees fix their own missed punch
@@ -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.52.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.52.0
4
+ version: 0.54.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kshitiz sinha
@@ -79,6 +79,7 @@ files:
79
79
  - app/assets/javascripts/hr_lite/share.js
80
80
  - app/assets/javascripts/hr_lite/tax_preview.js
81
81
  - app/assets/javascripts/hr_lite/vendor/html_to_image.js
82
+ - app/assets/javascripts/hr_lite/vendor/turbo.min.js
82
83
  - app/assets/stylesheets/hr_lite/application.css
83
84
  - app/assets/stylesheets/hr_lite/hr_lite.css
84
85
  - app/controllers/hr_lite/admin/appraisals_controller.rb