hr_lite 0.54.0 → 0.55.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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/app/assets/javascripts/hr_lite/confirm.js +9 -5
  4. data/app/assets/javascripts/hr_lite/countdown.js +9 -2
  5. data/app/assets/javascripts/hr_lite/search.js +1 -1
  6. data/app/assets/stylesheets/hr_lite/hr_lite.css +18 -9
  7. data/app/controllers/hr_lite/admin/attendances_controller.rb +10 -3
  8. data/app/controllers/hr_lite/admin/audit_logs_controller.rb +1 -0
  9. data/app/controllers/hr_lite/admin/employees_controller.rb +11 -4
  10. data/app/controllers/hr_lite/admin/expenses_controller.rb +4 -0
  11. data/app/controllers/hr_lite/admin/hr_requests_controller.rb +4 -0
  12. data/app/controllers/hr_lite/admin/leave_requests_controller.rb +4 -1
  13. data/app/controllers/hr_lite/admin/payroll_runs_controller.rb +9 -5
  14. data/app/controllers/hr_lite/admin/reports_controller.rb +3 -2
  15. data/app/controllers/hr_lite/admin/salary_structures_controller.rb +2 -0
  16. data/app/controllers/hr_lite/admin/tax_declarations_controller.rb +13 -3
  17. data/app/controllers/hr_lite/approvals_controller.rb +1 -0
  18. data/app/controllers/hr_lite/home_controller.rb +2 -2
  19. data/app/controllers/hr_lite/regularization_requests_controller.rb +4 -4
  20. data/app/controllers/hr_lite/tax_declarations_controller.rb +9 -5
  21. data/app/helpers/hr_lite/application_helper.rb +14 -8
  22. data/app/jobs/hr_lite/attendance_close_job.rb +1 -1
  23. data/app/jobs/hr_lite/payroll_compute_job.rb +12 -0
  24. data/app/models/concerns/hr_lite/audited.rb +3 -1
  25. data/app/models/concerns/hr_lite/encrypted_money.rb +8 -5
  26. data/app/models/hr_lite/attendance_record.rb +14 -0
  27. data/app/models/hr_lite/comp_off_request.rb +2 -0
  28. data/app/models/hr_lite/document.rb +1 -1
  29. data/app/models/hr_lite/expense.rb +3 -0
  30. data/app/models/hr_lite/hr_request.rb +2 -0
  31. data/app/models/hr_lite/kudo.rb +2 -2
  32. data/app/models/hr_lite/leave_request.rb +25 -6
  33. data/app/models/hr_lite/payroll_line_item.rb +8 -0
  34. data/app/models/hr_lite/payroll_run.rb +15 -6
  35. data/app/models/hr_lite/regularization_request.rb +2 -2
  36. data/app/models/hr_lite/resignation.rb +5 -1
  37. data/app/models/hr_lite/role.rb +5 -0
  38. data/app/models/hr_lite/salary_slip.rb +1 -1
  39. data/app/models/hr_lite/tax_declaration.rb +5 -0
  40. data/app/services/hr_lite/attendance_puncher.rb +4 -0
  41. data/app/services/hr_lite/overview_query.rb +0 -12
  42. data/app/services/hr_lite/work_status.rb +5 -2
  43. data/app/views/hr_lite/admin/assets/index.html.erb +9 -2
  44. data/app/views/hr_lite/admin/attendances/show.html.erb +3 -1
  45. data/app/views/hr_lite/admin/attendances/summary.html.erb +5 -2
  46. data/app/views/hr_lite/admin/audit_logs/index.html.erb +6 -4
  47. data/app/views/hr_lite/admin/ca_reviews/index.html.erb +3 -3
  48. data/app/views/hr_lite/admin/checklists/index.html.erb +3 -3
  49. data/app/views/hr_lite/admin/comp_off_requests/index.html.erb +1 -2
  50. data/app/views/hr_lite/admin/comp_off_requests/show.html.erb +5 -5
  51. data/app/views/hr_lite/admin/designation_changes/new.html.erb +4 -3
  52. data/app/views/hr_lite/admin/documents/index.html.erb +8 -3
  53. data/app/views/hr_lite/admin/documents/show.html.erb +1 -0
  54. data/app/views/hr_lite/admin/employees/_form.html.erb +12 -5
  55. data/app/views/hr_lite/admin/employees/show.html.erb +38 -32
  56. data/app/views/hr_lite/admin/expenses/index.html.erb +3 -2
  57. data/app/views/hr_lite/admin/holidays/index.html.erb +5 -4
  58. data/app/views/hr_lite/admin/hr_requests/index.html.erb +1 -1
  59. data/app/views/hr_lite/admin/hr_requests/show.html.erb +4 -3
  60. data/app/views/hr_lite/admin/leave_balances/index.html.erb +6 -4
  61. data/app/views/hr_lite/admin/leave_requests/index.html.erb +1 -2
  62. data/app/views/hr_lite/admin/leave_requests/show.html.erb +5 -3
  63. data/app/views/hr_lite/admin/leave_types/edit.html.erb +4 -4
  64. data/app/views/hr_lite/admin/leave_types/index.html.erb +3 -3
  65. data/app/views/hr_lite/admin/loans/index.html.erb +1 -1
  66. data/app/views/hr_lite/admin/loans/show.html.erb +8 -12
  67. data/app/views/hr_lite/admin/office_locations/edit.html.erb +4 -4
  68. data/app/views/hr_lite/admin/payroll_runs/index.html.erb +2 -2
  69. data/app/views/hr_lite/admin/payroll_runs/show.html.erb +10 -7
  70. data/app/views/hr_lite/admin/policies/index.html.erb +1 -1
  71. data/app/views/hr_lite/admin/policies/new.html.erb +2 -2
  72. data/app/views/hr_lite/admin/regularization_requests/index.html.erb +3 -4
  73. data/app/views/hr_lite/admin/regularization_requests/show.html.erb +4 -5
  74. data/app/views/hr_lite/admin/reports/index.html.erb +3 -6
  75. data/app/views/hr_lite/admin/reports/show.html.erb +16 -2
  76. data/app/views/hr_lite/admin/roles/_form.html.erb +6 -5
  77. data/app/views/hr_lite/admin/salary_slips/show.html.erb +3 -2
  78. data/app/views/hr_lite/admin/salary_structures/_form.html.erb +1 -0
  79. data/app/views/hr_lite/admin/settings/edit.html.erb +5 -1
  80. data/app/views/hr_lite/admin/statutory_rate_cards/_form.html.erb +3 -3
  81. data/app/views/hr_lite/admin/statutory_rate_cards/index.html.erb +3 -1
  82. data/app/views/hr_lite/admin/tax_declarations/index.html.erb +1 -1
  83. data/app/views/hr_lite/admin/tax_declarations/show.html.erb +2 -0
  84. data/app/views/hr_lite/admin/wfh_requests/index.html.erb +17 -16
  85. data/app/views/hr_lite/admin/wfh_requests/show.html.erb +2 -4
  86. data/app/views/hr_lite/admin/work_locations/index.html.erb +1 -1
  87. data/app/views/hr_lite/admin/work_modes/index.html.erb +1 -1
  88. data/app/views/hr_lite/approvals/index.html.erb +1 -1
  89. data/app/views/hr_lite/attendance/_month_grid.html.erb +9 -9
  90. data/app/views/hr_lite/attendance/_penalties.html.erb +2 -2
  91. data/app/views/hr_lite/attendance/_punch_card.html.erb +2 -4
  92. data/app/views/hr_lite/attendance/_slot.html.erb +9 -6
  93. data/app/views/hr_lite/attendance/_trail.html.erb +1 -1
  94. data/app/views/hr_lite/attendance/_work_mode.html.erb +1 -1
  95. data/app/views/hr_lite/attendance/show.html.erb +4 -4
  96. data/app/views/hr_lite/calendar/show.html.erb +2 -2
  97. data/app/views/hr_lite/comp_off_requests/index.html.erb +1 -1
  98. data/app/views/hr_lite/comp_off_requests/new.html.erb +1 -1
  99. data/app/views/hr_lite/documents/_upload_form.html.erb +1 -1
  100. data/app/views/hr_lite/employee_profiles/show.html.erb +4 -2
  101. data/app/views/hr_lite/home/index.html.erb +2 -2
  102. data/app/views/hr_lite/id_cards/show.html.erb +5 -1
  103. data/app/views/hr_lite/leave_requests/_balance_chips.html.erb +2 -2
  104. data/app/views/hr_lite/leave_requests/new.html.erb +17 -8
  105. data/app/views/hr_lite/leave_requests/show.html.erb +2 -0
  106. data/app/views/hr_lite/org/show.html.erb +1 -1
  107. data/app/views/hr_lite/regularization_requests/index.html.erb +7 -7
  108. data/app/views/hr_lite/regularization_requests/new.html.erb +7 -5
  109. data/app/views/hr_lite/salary_slips/_slip_detail.html.erb +2 -2
  110. data/app/views/hr_lite/salary_slips/pdf.html.erb +1 -1
  111. data/app/views/hr_lite/tax/_computation.html.erb +1 -1
  112. data/app/views/hr_lite/tax_declarations/show.html.erb +4 -4
  113. data/app/views/hr_lite/team/show.html.erb +8 -7
  114. data/app/views/hr_lite/timelines/show.html.erb +1 -0
  115. data/app/views/hr_lite/wfh_requests/index.html.erb +3 -3
  116. data/app/views/hr_lite/work_time/_bars.html.erb +1 -1
  117. data/app/views/hr_lite/work_time/_donut.html.erb +1 -1
  118. data/app/views/hr_lite/work_time/_report.html.erb +14 -8
  119. data/app/views/layouts/hr_lite/application.html.erb +1 -0
  120. data/lib/hr_lite/notifications.rb +5 -2
  121. data/lib/hr_lite/version.rb +1 -1
  122. metadata +2 -1
@@ -14,7 +14,7 @@
14
14
  <div class="hrl-row hrl-right">
15
15
  <% { present: "ok", half_day: "ok", leave: nil, absent: "bad" }.each do |kind, tone| %>
16
16
  <% code, word = HrLite::ApplicationHelper::DAY_STATES[kind] %>
17
- <span class="<%= class_names("hrl-badge", "hrl-badge--#{tone}": tone) %>" role="img" aria-label="<%= word %> <%= pluralize(@counts[kind], "day") %>"><%= code %> <%= @counts[kind] %></span>
17
+ <span class="<%= class_names("hrl-badge", "hrl-badge--#{tone}": tone) %>" role="img" aria-label="<%= word %> <%= pluralize(@counts[kind], "day") %>"><%= word %> <%= @counts[kind] %></span>
18
18
  <% end %>
19
19
  </div>
20
20
  </div>
@@ -22,8 +22,8 @@
22
22
  link_builder: ->(d) { hr_lite.attendance_path(month: @month.strftime("%Y-%m"), day: d.iso8601, anchor: "day") if d <= Date.current } %>
23
23
  <p class="hrl-small hrl-muted hrl-mt">Forgot to check in or out on some day?</p>
24
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>
25
+ <a class="hrl-btn" href="<%= hr_lite.new_regularization_request_path %>">Fix a day</a>
26
+ <a class="hrl-btn" href="<%= hr_lite.regularization_requests_path %>">Day fixes</a>
27
27
  </div>
28
28
  </section>
29
29
 
@@ -48,7 +48,7 @@
48
48
  <% if leave %><dt>Leave</dt><dd><%= leave.leave_type.name %></dd><% end %>
49
49
  <% if @day_info.kind == :holiday %><dt>Holiday</dt><dd><%= HrLite::Holiday.company_wide.for_office(HrLite::WorkingCalendar.office_for(hr_current_user.id)).where(date: @day).pick(:name) %></dd><% end %>
50
50
  <% if rec&.flagged? %><dt>Flag</dt><dd><%= rec.flag_note %></dd><% end %>
51
- <% if @day_ticket %><dt>Ticket</dt><dd><%= hrl_request_status_badge(@day_ticket.status) %> <%= @day_ticket.times_label %></dd><% end %>
51
+ <% if @day_ticket %><dt>Day fix</dt><dd><%= hrl_request_status_badge(@day_ticket.status) %> <%= @day_ticket.times_label %></dd><% end %>
52
52
  </dl>
53
53
  <% unless %i[holiday weekend leave].include?(@day_info.kind) || @day_ticket&.pending? %>
54
54
  <a class="hrl-btn hrl-btn--primary hrl-btn--block hrl-mt" href="<%= hr_lite.new_regularization_request_path(date: @day) %>">Fix this day</a>
@@ -20,7 +20,7 @@
20
20
  <% (first.cwday - 1).times do %><div></div><% end %>
21
21
  <% (first..@month.end_of_month).each do |date| %>
22
22
  <% cell_class, code =
23
- if @holidays[date] then [ "hrl-mgrid__day--holiday", "H" ]
23
+ if @calendar.holiday?(date) then [ "hrl-mgrid__day--holiday", "H" ]
24
24
  elsif @calendar.weekend?(date) then "hrl-mgrid__day--weekend"
25
25
  elsif @leaves[date].any? then [ "hrl-mgrid__day--leave", "#{@leaves[date].size} off" ]
26
26
  end %>
@@ -40,7 +40,7 @@
40
40
 
41
41
  <%# Names only — the leave type is the employee's own business. Leave keeps its label's "(first half)" note. %>
42
42
  <% day = ->(d) { d.strftime("%a %-d %b") }
43
- agenda = @holidays.values.flatten.map { |h| [ h.date, "#{day.(h.date)}: #{h.name} (holiday)" ] } +
43
+ agenda = @holidays.values.flatten.map { |h| [ h.date, "#{day.(h.date)}: #{h.name} (#{h.optional ? 'optional holiday' : 'holiday'})" ] } +
44
44
  @leaves.values.flatten.uniq.map { |r| [ [ r.start_date, first ].max, "#{[ r.start_date, r.end_date ].uniq.map(&day).join(' – ')}#{r.date_range_label[/ \(.+\)/]}: #{hr_display_name(r.user)}, on leave" ] } %>
45
45
  <% if agenda.any? %>
46
46
  <ul class="hrl-feed hrl-small hrl-mt">
@@ -9,7 +9,7 @@
9
9
 
10
10
  <section class="hrl-card">
11
11
  <p class="hrl-small hrl-muted">
12
- Worked on a weekend or holiday? Request a comp-off — once an admin approves,
12
+ Worked on a weekend or holiday? Request a comp-off — once it is approved,
13
13
  <%= @comp_off_type ? "your #{@comp_off_type.name} balance is credited" : "a day is credited to your balance" %>
14
14
  and you spend it through the normal leave flow.
15
15
  </p>
@@ -18,7 +18,7 @@
18
18
  <%= f.label :half_day, "I worked only half the day (credits 0.5)" %>
19
19
  </div>
20
20
  <div class="hrl-field">
21
- <%= f.label :reason, "What did you work on? (required)" %>
21
+ <%= f.label :reason, "What did you work on?" %>
22
22
  <%= f.text_area :reason, required: true %>
23
23
  </div>
24
24
  <div class="hrl-form-actions">
@@ -1,6 +1,6 @@
1
1
  <%# locals: document, url (where it posts), open (start expanded) — one upload form for My documents and HR's view of a person. %>
2
2
  <details class="hrl-card" <%= "open" if open || document.errors.any? %>>
3
- <summary class="hrl-btn hrl-btn--primary">Upload a document</summary>
3
+ <summary class="hrl-btn">Upload a document</summary>
4
4
  <%= form_with model: document, url: url, method: :post, local: true, class: "hrl-mt" do |f| %>
5
5
  <%= render "hr_lite/shared/form_errors", record: document %>
6
6
  <div class="hrl-field">
@@ -19,8 +19,10 @@
19
19
  <dt>Bank</dt><dd><%= [ @profile.bank_name, @profile.masked_account ].compact.join(" ").presence || "—" %></dd>
20
20
  </dl>
21
21
  <a class="hrl-btn hrl-mt" href="<%= hr_lite.org_path %>">Org chart</a>
22
- <p class="hrl-small hrl-muted hrl-mt">Something wrong? Ask leadership to correct it — changes are audited.</p>
23
- <a class="hrl-btn hrl-mt" href="<%= hr_lite.resignation_path %>">Resignation</a>
22
+ <p class="hrl-small hrl-muted hrl-mt">Something wrong?
23
+ <%= link_to_if hr_can?("hr_request.raise"), "Ask leadership to correct it", hr_lite.new_hr_request_path, class: "hrl-card__link" %> — changes are audited.</p>
24
+ <hr>
25
+ <a class="hrl-btn" href="<%= hr_lite.resignation_path %>">Resignation</a>
24
26
  </section>
25
27
  <% else %>
26
28
  <p class="hrl-card hrl-card--empty">Your HR profile has not been set up yet.</p>
@@ -20,7 +20,8 @@
20
20
  <% @mine_pending.each do |req| %>
21
21
  <% title, detail, href = case req
22
22
  when HrLite::LeaveRequest then [ req.leave_type.name, "#{req.date_range_label} · #{pluralize(hrl_days(req.days_count), 'day')}", hr_lite.leave_request_path(req) ]
23
- when HrLite::RegularizationRequest then [ "Attendance fix", "#{req.date.strftime('%a %-d %b')} · #{req.times_label}", hr_lite.regularization_requests_path ]
23
+ when HrLite::RegularizationRequest then [ "Day fix", "#{req.date.strftime('%a %-d %b')} · #{req.times_label}", hr_lite.regularization_requests_path ]
24
+ when HrLite::WfhRequest then [ "Work from home", [ req.start_date, req.end_date ].uniq.map { |d| d.strftime("%a %-d %b") }.join(" – "), hr_lite.wfh_requests_path ]
24
25
  else [ "Comp-off", "Worked #{req.date_worked.strftime('%a %-d %b')}", hr_lite.comp_off_requests_path ]
25
26
  end %>
26
27
  <a class="hrl-listrow" href="<%= href %>">
@@ -37,7 +38,6 @@
37
38
  <section class="hrl-card">
38
39
  <h2 class="hrl-card__title">On leave this week</h2>
39
40
  <%= render "hr_lite/leave_requests/out_list", leaves: @out_week, empty: "Nobody is on leave this week." %>
40
- <a class="hrl-card__link" href="<%= hr_lite.team_path %>">Team board</a>
41
41
  </section>
42
42
 
43
43
  <% if @awards.any? %>
@@ -1,5 +1,6 @@
1
1
  <% content_for(:page_title) { "ID card" } %>
2
2
  <% content_for(:tabrow) { render "hr_lite/shared/person_tabs", user: @user } if params[:user_id] %>
3
+ <% content_for(:back, "Employees") if params[:user_id] %>
3
4
  <% company = HrLite.config.company.call %>
4
5
  <% brand = company[:brand].presence || company[:name].presence || "HR" %>
5
6
  <% own = @user == hr_current_user %>
@@ -24,7 +25,7 @@
24
25
  </div>
25
26
  <div class="hrl-idcard__photo">
26
27
  <% if @profile.photo.attached? %>
27
- <img src="<%= Rails.application.routes.url_helpers.rails_storage_proxy_path(@profile.photo, disposition: "inline", only_path: true) %>" alt="<%= hr_display_name(@user) %>">
28
+ <img src="<%= Rails.application.routes.url_helpers.rails_storage_proxy_path(@profile.photo, disposition: "inline", only_path: true, expires_in: 1.hour) %>" alt="<%= hr_display_name(@user) %>">
28
29
  <% else %>
29
30
  <span><%= hr_display_name(@user).to_s.scan(/\b[[:alpha:]]/).first(2).join.upcase %></span>
30
31
  <% end %>
@@ -63,6 +64,9 @@
63
64
  </div>
64
65
 
65
66
  <div class="hrl-idcard-actions">
67
+ <% if own && (@profile.blood_group.blank? || @profile.emergency_contact.blank?) && hr_can?("hr_request.raise") %>
68
+ <a class="hrl-card__link hrl-idcard-help" href="<%= hr_lite.new_hr_request_path %>">Missing details? Ask HR</a>
69
+ <% end %>
66
70
  <button type="button" class="hrl-btn hrl-btn--primary" data-hrl-print>Print</button>
67
71
  <button type="button" class="hrl-btn" data-hrl-share="id-card-<%= @profile.employee_code %>.png">Share</button>
68
72
  <% if own && @print_requested %>
@@ -6,10 +6,10 @@
6
6
  <% if types.any? %>
7
7
  <div class="hrl-row">
8
8
  <% types.each do |type| %>
9
- <% balance = HrLite::LeaveBalance.for(user, type, HrLite::LeaveYear.current_key) %>
9
+ <% balance = HrLite::LeaveBalance.for(user, type, HrLite::LeaveYear.current_key); entitled = balance.entitled %>
10
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) %>
11
11
  <span class="hrl-badge hrl-badge--type" style="--hrl-type: <%= type.color %>">
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
+ <%= type.name %> · <%= hrl_days(entitled - balance.used) %> of <%= hrl_days(entitled) %> left<% if pending.positive? %> · <%= hrl_days(pending) %> pending<% end %><% if (held = balance.penalty_days).positive? %> · <%= hrl_days(held) %> held for penalties<% end %>
13
13
  </span>
14
14
  <% end %>
15
15
  </div>
@@ -7,38 +7,47 @@
7
7
  <section class="hrl-card">
8
8
  <%= form_with model: @request, url: hr_lite.leave_requests_path, local: true do |f| %>
9
9
  <%= render "hr_lite/shared/form_errors", record: @request %>
10
+ <% bad = ->(attr) { "hrl-#{attr}-error" if @request.errors[attr].any? } %>
11
+ <% say = ->(attr) { tag.p(@request.errors.full_messages_for(attr).to_sentence, id: bad.(attr), class: "hrl-hint hrl-hint--bad") if bad.(attr) } %>
10
12
  <div class="hrl-field">
11
13
  <%= f.label :leave_type_id, "Type", id: "hrl-leave-type", for: nil %>
12
14
  <% 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
+ <%= tag.div(class: "hrl-chiprow", role: "radiogroup", aria: { labelledby: "hrl-leave-type", describedby: bad.(:leave_type) }) do %>
16
+ <% HrLite::LeaveType.active.reject { |t| t.comp_off && (b = left[t.id]) && b.available <= 0 }.each_with_index do |type, i| %>
15
17
  <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] %><% if left[type.id] && (held = left[type.id].penalty_days).positive? %> · <%= hrl_days(held) %> held for penalties<% end %><%= " · Unpaid" unless type.paid %></span></label>
16
18
  <% end %>
17
- </div>
19
+ <% end %>
20
+ <%= say.(:leave_type) %>
18
21
  <p class="hrl-hint">Days beyond your balance are unpaid (loss of pay), cut from that month's salary.<% if (capped = @balances.find { |b| b.penalty_days.positive? }) %> Today you can apply for at most <%= hrl_days(capped.applicable) %> days of <%= capped.leave_type.name %>, <%= hrl_days(capped.penalty_days) %> held for penalties.<% end %></p>
22
+ <% if (profile = HrLite::EmployeeProfile.find_by(user_id: hr_current_user.id))&.on_probation? %>
23
+ <p class="hrl-hint">On probation until <%= profile.probation_until.strftime("%d %b %Y") %>: at most 1 day of leave a month, of any type.</p>
24
+ <% end %>
19
25
  </div>
20
26
  <div class="hrl-field">
21
27
  <%= f.label :start_date, "From" %>
22
- <%= f.date_field :start_date, required: true %>
28
+ <%= f.date_field :start_date, required: true, aria: { describedby: bad.(:start_date) } %>
29
+ <%= say.(:start_date) %>
23
30
  </div>
24
31
  <div class="hrl-field">
25
32
  <%= f.label :end_date, "To (leave blank for one day)" %>
26
- <%= f.date_field :end_date %>
33
+ <%= f.date_field :end_date, aria: { describedby: bad.(:end_date) } %>
34
+ <%= say.(:end_date) %>
27
35
  </div>
28
36
  <div class="hrl-field">
29
37
  <%= f.label :half_day_part, "Day", id: "hrl-day-part", for: nil %>
30
- <div class="hrl-chiprow" role="radiogroup" aria-labelledby="hrl-day-part">
38
+ <%= tag.div(class: "hrl-chiprow", role: "radiogroup", aria: { labelledby: "hrl-day-part", describedby: bad.(:half_day) }) do %>
31
39
  <% HrLite::DAY_PARTS.each do |label, value| %>
32
40
  <label><%= f.radio_button :half_day_part, value %><span><%= label %></span></label>
33
41
  <% end %>
34
- </div>
42
+ <% end %>
43
+ <%= say.(:half_day) %>
35
44
  </div>
36
45
  <div class="hrl-field">
37
46
  <%= f.label :reason, "Reason (optional)" %>
38
47
  <%= f.text_area :reason %>
39
48
  </div>
40
49
  <div class="hrl-form-actions">
41
- <%= f.submit "Submit request", class: "hrl-btn hrl-btn--primary" %>
50
+ <%= f.submit "Send for approval", class: "hrl-btn hrl-btn--primary" %>
42
51
  <a class="hrl-btn" href="<%= hr_lite.leave_requests_path %>">Cancel</a>
43
52
  </div>
44
53
  <% end %>
@@ -19,5 +19,7 @@
19
19
  <%= button_to "Cancel this leave", hr_lite.cancel_leave_request_path(@request), method: :post,
20
20
  class: "hrl-btn hrl-btn--danger hrl-mt",
21
21
  form: { data: { turbo_confirm: "Cancel this leave request?" } } %>
22
+ <% elsif @request.approved? %>
23
+ <p class="hrl-hint hrl-mt">This leave has started, so it can no longer be cancelled here. Ask HR if it needs cancelling.</p>
22
24
  <% end %>
23
25
  </section>
@@ -1,6 +1,6 @@
1
1
  <% content_for(:page_title) { "Org chart" } %>
2
2
  <div class="hrl-page__head">
3
- <h1 class="hrl-page__title">Organisation</h1>
3
+ <h1 class="hrl-page__title">Org chart</h1>
4
4
  </div>
5
5
 
6
6
  <% if @own_chain.any? %>
@@ -1,16 +1,16 @@
1
- <% content_for(:page_title) { "Regularization" } %>
1
+ <% content_for(:page_title) { "Day fixes" } %>
2
2
  <% content_for(:back, "Attendance") %>
3
3
  <div class="hrl-page__head">
4
- <h1 class="hrl-page__title">My regularization tickets</h1>
4
+ <h1 class="hrl-page__title">Day fixes</h1>
5
5
  <div class="hrl-page__actions">
6
- <a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_regularization_request_path %>">Raise a ticket</a>
6
+ <a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_regularization_request_path %>">Fix a day</a>
7
7
  </div>
8
8
  </div>
9
9
 
10
10
  <section class="hrl-card">
11
11
  <p class="hrl-small hrl-muted">
12
- Forgot to check in or out? Raise a ticket with the actual times — once an
13
- admin approves, your attendance for that day is fixed automatically.
12
+ Forgot to check in or out? Fix a day with the actual times — once your
13
+ manager or HR approves, your attendance for that day is fixed automatically.
14
14
  </p>
15
15
  <% if @requests.any? %>
16
16
  <div class="hrl-table-wrap">
@@ -26,7 +26,7 @@
26
26
  <td data-label="" class="<%= 'hrl-cell--void' unless request.pending? %>">
27
27
  <% if request.pending? %>
28
28
  <%= button_to "Cancel", hr_lite.cancel_regularization_request_path(request), method: :post,
29
- class: "hrl-btn hrl-btn--danger", form: { data: { turbo_confirm: "Cancel this ticket?" } } %>
29
+ class: "hrl-btn hrl-btn--danger", form: { data: { turbo_confirm: "Cancel this day fix?" } } %>
30
30
  <% end %>
31
31
  </td>
32
32
  </tr>
@@ -36,6 +36,6 @@
36
36
  </div>
37
37
  <%= hrl_pagination %>
38
38
  <% else %>
39
- <p class="hrl-card--empty">No tickets yet.</p>
39
+ <p class="hrl-card--empty">No day fixes yet.</p>
40
40
  <% end %>
41
41
  </section>
@@ -1,14 +1,14 @@
1
- <% content_for(:page_title) { "Raise a regularization ticket" } %>
1
+ <% content_for(:page_title) { "Fix a day" } %>
2
2
  <% content_for(:back, "Attendance") %>
3
3
  <div class="hrl-page__head">
4
- <h1 class="hrl-page__title">Raise a regularization ticket</h1>
4
+ <h1 class="hrl-page__title">Fix a day</h1>
5
5
  </div>
6
6
 
7
7
  <section class="hrl-card">
8
8
  <%= form_with model: @request, url: hr_lite.regularization_requests_path, scope: :regularization_request, local: true do |f| %>
9
9
  <%= render "hr_lite/shared/form_errors", record: @request %>
10
10
  <% if (rule = HrLite.config.self_regularization) %>
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
+ <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 your manager or HR for approval.</p>
12
12
  <% end %>
13
13
  <div class="hrl-field">
14
14
  <%= f.label :date, "Which day needs fixing?" %>
@@ -25,11 +25,13 @@
25
25
  <p class="hrl-hint">Leave blank if only check-in is missing. Short day? Leave both times blank and give the reason.</p>
26
26
  </div>
27
27
  <div class="hrl-field">
28
- <%= f.label :reason, "Why does this day need fixing? (required)" %>
28
+ <%= f.label :reason, "Why does this day need fixing?" %>
29
29
  <%= f.text_area :reason, required: true %>
30
30
  </div>
31
+ <% fix_now = rule && @request.date && (probe = HrLite::RegularizationRequest.new(date: @request.date, user_id: hr_current_user.id)).self_fixable? %>
32
+ <% if fix_now %><p class="hrl-hint"><%= @request.date.strftime("%a %-d %b") %> is fixed as soon as you submit. Self-fixes left this week: <%= probe.self_fixes_left %>.</p><% end %>
31
33
  <div class="hrl-form-actions">
32
- <%= f.submit "Raise ticket", class: "hrl-btn hrl-btn--primary" %>
34
+ <%= f.submit fix_now ? "Fix now" : "Send for approval", class: "hrl-btn hrl-btn--primary" %>
33
35
  <a class="hrl-btn" href="<%= hr_lite.regularization_requests_path %>">Cancel</a>
34
36
  </div>
35
37
  <% end %>
@@ -3,7 +3,7 @@
3
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
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
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>
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 / <abbr title="Loss of pay: unpaid days">LOP</abbr> days</div></div>
7
7
  </div>
8
8
  <div class="hrl-grid hrl-grid--2">
9
9
  <section class="hrl-card">
@@ -46,7 +46,7 @@
46
46
  <%# Out-of-window days are named rather than silently dropped: without them
47
47
  a mid-month joiner's row does not add up to the month. %>
48
48
  <dt>Days</dt>
49
- <dd><%= hrl_days(slip.payable_days) %> payable / <%= hrl_days(slip.effective_lop_days) %> LOP<% if slip.out_of_window_days.to_f.positive? %>
49
+ <dd><%= hrl_days(slip.payable_days) %> payable / <%= hrl_days(slip.effective_lop_days) %> <abbr title="Loss of pay: unpaid days">LOP</abbr><% if slip.out_of_window_days.to_f.positive? %>
50
50
  / <%= hrl_days(slip.out_of_window_days) %> outside employment<% end %> of <%= slip.days_in_month %></dd>
51
51
  <% if profile %>
52
52
  <dt>Employee code</dt><dd><%= profile.employee_code %></dd>
@@ -33,7 +33,7 @@
33
33
  </td>
34
34
  </tr>
35
35
  <tr>
36
- <td>Payable days: <%= @slip.payable_days %> / <%= @slip.days_in_month %> (LOP <%= @slip.effective_lop_days %>)</td>
36
+ <td>Payable days: <%= @slip.payable_days.to_s.delete_suffix(".0") %> / <%= @slip.days_in_month %> (unpaid <%= @slip.effective_lop_days.to_s.delete_suffix(".0") %>)</td>
37
37
  <td class="num"><% if @profile&.masked_account %>Bank: <%= @profile.bank_name %> <%= @profile.masked_account %><% end %></td>
38
38
  </tr>
39
39
  </tbody>
@@ -34,7 +34,7 @@
34
34
  <table class="hrl-table hrl-tax-working__table">
35
35
  <tbody>
36
36
  <tr><td>Salary paid so far this year</td><td class="hrl-num"><%= hrl_rupees(tax.paid_gross) %></td></tr>
37
- <tr><td>Expected for the rest of the year (<%= hrl_rupees(tax.structure.monthly_gross) %> × <%= s.months_left %>)</td><td class="hrl-num"><%= hrl_rupees(tax.projected_gross) %></td></tr>
37
+ <tr><td>Expected for the rest of the year, before unpaid days (<%= hrl_rupees(tax.structure.monthly_gross) %> × <%= s.months_left %>)</td><td class="hrl-num"><%= hrl_rupees(tax.projected_gross) %></td></tr>
38
38
  <tr class="hrl-tax-working__total"><td>Gross salary for the year</td><td class="hrl-num"><%= hrl_rupees(s.gross) %></td></tr>
39
39
  <% if s.hra_exemption.positive? %><tr><td>Less HRA exemption (from rent)</td><td class="hrl-num">− <%= hrl_rupees(s.hra_exemption) %></td></tr><% end %>
40
40
  <tr><td>Less standard deduction</td><td class="hrl-num">− <%= hrl_rupees(s.standard_deduction) %></td></tr>
@@ -1,6 +1,6 @@
1
1
  <% content_for(:page_title) { "Tax" } %>
2
2
  <% submittable = @declaration.persisted? && (@declaration.draft? || @declaration.rejected?) %>
3
- <% locked = @declaration.verified? %>
3
+ <% locked = @declaration.verified? || @declaration.submitted? %>
4
4
  <div class="hrl-page__head">
5
5
  <h1 class="hrl-page__title">Tax — <%= @declaration.label %></h1>
6
6
  </div>
@@ -13,7 +13,7 @@
13
13
 
14
14
  <section class="hrl-card">
15
15
  <p>
16
- Declaration: <%= hrl_request_status_badge(@declaration.status == "verified" ? "approved" : @declaration.status) %>
16
+ Declaration: <% if @declaration.verified? %><span class="hrl-badge hrl-badge--ok">Verified</span><% elsif @declaration.rejected? %><span class="hrl-badge hrl-badge--bad">Sent back</span><% else %><%= hrl_request_status_badge(@declaration.status) %><% end %>
17
17
  <% if @declaration.note.present? %><span class="hrl-small hrl-muted">— <%= @declaration.note %></span><% end %>
18
18
  </p>
19
19
  <fieldset class="hrl-field" <%= "disabled" if locked %>>
@@ -26,7 +26,7 @@
26
26
  </div>
27
27
  <% end %>
28
28
  </fieldset>
29
- </details>
29
+ </section>
30
30
 
31
31
  <details class="hrl-card" <%= "open" if @declaration.regime != "new" || @declaration.errors.any? %>>
32
32
  <summary class="hrl-btn">Investments and rent (old regime only)</summary>
@@ -69,7 +69,7 @@
69
69
  <% if submittable %><button type="submit" name="submit_to_hr" value="1" class="hrl-btn hrl-btn--primary">Submit to HR</button><% end %>
70
70
  </div>
71
71
  <% else %>
72
- <p class="hrl-small hrl-muted">HR has verified this year's declaration. Ask HR to reopen it to change anything.</p>
72
+ <p class="hrl-small hrl-muted"><%= @declaration.verified? ? "HR has verified this year's declaration." : "Sent to HR to check the proof." %> To change anything, ask HR through Help › Ask HR.</p>
73
73
  <% end %>
74
74
  <% end %>
75
75
  <% content_for(:scripts) { javascript_include_tag "hr_lite/tax_preview", defer: true } %>
@@ -1,13 +1,13 @@
1
- <% content_for(:page_title) { "Team" } %>
1
+ <% content_for(:page_title) { "Team board" } %>
2
2
  <div class="hrl-page__head">
3
- <h1 class="hrl-page__title">Team — <%= @date == Date.current ? "today" : @date.strftime("%A, %d %B") %></h1>
3
+ <h1 class="hrl-page__title">Team board — <%= @date == Date.current ? "today" : @date.strftime("%A, %d %B") %></h1>
4
4
  <div class="hrl-page__actions">
5
- <a class="hrl-btn" aria-label="Previous day" href="<%= hr_lite.team_path(date: (@date - 1).strftime("%Y-%m-%d")) %>">&larr;</a>
5
+ <a class="hrl-btn" href="<%= hr_lite.team_path(date: (@date - 1).strftime("%Y-%m-%d")) %>"><span aria-hidden="true">&lsaquo;</span> <%= @date == Date.current ? "Yesterday" : "Previous day" %></a>
6
6
  <% unless @date == Date.current %>
7
7
  <a class="hrl-btn" href="<%= hr_lite.team_path %>">Today</a>
8
8
  <% end %>
9
9
  <% if @date < Date.current %>
10
- <a class="hrl-btn" aria-label="Next day" href="<%= hr_lite.team_path(date: (@date + 1).strftime("%Y-%m-%d")) %>">&rarr;</a>
10
+ <a class="hrl-btn" href="<%= hr_lite.team_path(date: (@date + 1).strftime("%Y-%m-%d")) %>">Next day <span aria-hidden="true">&rsaquo;</span></a>
11
11
  <% end %>
12
12
  </div>
13
13
  </div>
@@ -17,7 +17,7 @@
17
17
  <div class="hrl-kpi"><div class="hrl-kpi__value"><%= kpis[:checked_in] %></div><div class="hrl-kpi__label">Checked in</div></div>
18
18
  <div class="hrl-kpi"><div class="hrl-kpi__value"><%= kpis[:on_leave] %></div><div class="hrl-kpi__label">On leave</div></div>
19
19
  <div class="hrl-kpi"><div class="hrl-kpi__value"><%= kpis[:not_in] %></div><div class="hrl-kpi__label">Not in</div></div>
20
- <div class="hrl-kpi"><div class="hrl-kpi__value"><%= @board.rows.size %></div><div class="hrl-kpi__label">Team size</div></div>
20
+ <div class="hrl-kpi"><div class="hrl-kpi__value"><%= @board.rows.size %></div><div class="hrl-kpi__label">People</div></div>
21
21
  </div>
22
22
 
23
23
  <% if @statuses %>
@@ -48,7 +48,8 @@
48
48
  <span class="hrl-badge hrl-badge--muted">+<%= row.profile.buffer_minutes / 60 %>h buffer · <%= HrLite::EmployeeProfile::BUFFER_REASONS[row.profile.buffer_reason] %></span><% end %></span>
49
49
  </td>
50
50
  <% if (st = @statuses&.dig(row.user.id)) %>
51
- <td data-label="Now"><span class="hrl-modes"><%= hrl_status_icons(st) %></span>
51
+ <%# A derived status (offline, leave, holiday) repeats the Status cell; phone cards skip it. %>
52
+ <td data-label="Now" class="<%= 'hrl-cell--void' unless st.primary.event || st.custom %>"><span class="hrl-modes"><%= hrl_status_icons(st) %></span>
52
53
  <span class="hrl-small"><%= st.primary.place || st.primary.label %><% if st.overlay %> · <%= st.overlay.label %><% end %>
53
54
  <% if (till = st.overlay&.until || (st.primary.until if st.primary.until.respond_to?(:hour))) %> · until <%= till.strftime("%H:%M") %><% end %></span>
54
55
  <% if st.updated_at %><span class="hrl-small hrl-muted"> · since <%= st.updated_at.strftime("%H:%M") %></span><% end %>
@@ -66,5 +67,5 @@
66
67
  <p class="hrl-card--empty">Nobody to show<%= @rows ? " for these filters" : " for this date" %>.</p>
67
68
  <% end %>
68
69
  <p class="hrl-small hrl-muted hrl-mt">Forgot a punch yourself?
69
- <a class="hrl-card__link" href="<%= hr_lite.new_regularization_request_path %>">Raise a regularization ticket</a></p>
70
+ <a class="hrl-card__link" href="<%= hr_lite.new_regularization_request_path %>">Fix a day</a></p>
70
71
  </section>
@@ -1,5 +1,6 @@
1
1
  <% content_for(:page_title) { "Timeline — #{hr_display_name(@person)}" } %>
2
2
  <% content_for(:tabrow) { render "hr_lite/shared/person_tabs", user: @person } if params[:user_id] %>
3
+ <% content_for(:back, "Org chart") if params[:user_id] %>
3
4
  <% year_path = ->(y) { @person.id == hr_current_user.id ? hr_lite.timeline_path(year: y) : hr_lite.person_timeline_path(@person.id, year: y) } %>
4
5
  <% name = hr_display_name(@person) %>
5
6
  <div class="hrl-page__head">
@@ -6,14 +6,14 @@
6
6
 
7
7
  <section class="hrl-card">
8
8
  <h2 class="hrl-card__title">Request WFH</h2>
9
- <p class="hrl-small hrl-muted">Pick dates from today up to <%= HrLite::WfhRequest::MAX_DAYS_AHEAD %> days ahead. HR approves or rejects. While a request is waiting you can still check in away from the office; HR sees the reason. A rejected date cannot.</p>
9
+ <p class="hrl-small hrl-muted">Pick dates from today up to <%= HrLite::WfhRequest::MAX_DAYS_AHEAD %> days ahead. Your manager or HR approves or rejects. While a request is waiting you can still check in away from the office; they see the reason. A rejected date cannot.</p>
10
10
  <%= form_with model: @request, url: hr_lite.wfh_requests_path, scope: :wfh_request, local: true do |f| %>
11
11
  <%= render "hr_lite/shared/form_errors", record: @request %>
12
12
  <% min, max = @window.first.iso8601, @window.last.iso8601 %>
13
13
  <div class="hrl-field"><%= f.label :start_date, "From" %><%= f.date_field :start_date, min: min, max: max, required: true %></div>
14
14
  <div class="hrl-field"><%= f.label :end_date, "To (same day for one day)" %><%= f.date_field :end_date, min: min, max: max, required: true %></div>
15
15
  <div class="hrl-field"><%= f.label :reason, "Why do you need WFH?" %><%= f.text_area :reason, rows: 3, required: true %></div>
16
- <div class="hrl-form-actions"><%= f.submit "Send to HR", class: "hrl-btn hrl-btn--primary" %></div>
16
+ <div class="hrl-form-actions"><%= f.submit "Send for approval", class: "hrl-btn hrl-btn--primary" %></div>
17
17
  <% end %>
18
18
  </section>
19
19
 
@@ -27,7 +27,7 @@
27
27
  <% code, word, mod = marks[@day_status[date]] %>
28
28
  <div class="<%= class_names("hrl-mgrid__day", "hrl-mgrid__day--#{mod}": mod, "hrl-mgrid__day--today": date == Date.current) %>"
29
29
  role="group" aria-label="<%= date.strftime('%-d %b') %><%= ", #{word}" if word %>">
30
- <span class="hrl-mgrid__num"><%= date.day %></span>
30
+ <span class="hrl-mgrid__num"><%= date.day == 1 ? date.strftime("%-d %b") : date.day %></span>
31
31
  <% if code %><span class="hrl-mgrid__tag"><%= code %></span><% end %>
32
32
  </div>
33
33
  <% end %>
@@ -2,7 +2,7 @@
2
2
  <% buckets = result.by_bucket
3
3
  totals = buckets.transform_values { |m| m.except("offline").values.sum }
4
4
  max = [ totals.values.max.to_f, 1 ].max
5
- w = 12; gap = 4; h = 100
5
+ w = 12; gap = 8; h = 100 # gap 8 units: bars ~11px apart on screen
6
6
  fmt = { day: "%-d", week: "%-d %b", month: "%b" }[period.bucket] %>
7
7
  <section class="hrl-card">
8
8
  <h2 class="hrl-card__title">By <%= period.bucket %></h2>
@@ -29,7 +29,7 @@
29
29
  </tbody>
30
30
  </table>
31
31
  </div>
32
- <p class="hrl-small hrl-muted">Shares are of tracked time (<%= hrl_duration(total) %>: working, breaks and out of office). Offline (<%= hrl_duration(result.offline) %>) is not included.</p>
32
+ <p class="hrl-small hrl-muted">Shares are of tracked time (<%= hrl_duration(total) %>: working, breaks and out of office). Offline (<%= hrl_duration(result.offline) %>) is not included. Stretches: how many separate times each mode ran.</p>
33
33
  <% else %>
34
34
  <p class="hrl-card--empty">Only offline time recorded.</p>
35
35
  <% end %>
@@ -20,12 +20,16 @@
20
20
  <a class="hrl-btn" href="<%= path.(period: period.kind, date: period.shift(1)) %>" aria-label="Next <%= period.kind %>">&rsaquo;</a>
21
21
  <% end %>
22
22
  </div>
23
- <%= form_with url: path.({}), method: :get, local: true, class: "hrl-row" do %>
24
- <input type="hidden" name="period" value="custom">
25
- <% (local_assigns[:keep] || {}).each do |k, v| %><input type="hidden" name="<%= k %>" value="<%= v %>"><% end %>
26
- <div class="hrl-field"><label for="wt-from">From</label><%= date_field_tag :from, period.from, max: Date.current, id: "wt-from" %></div>
27
- <div class="hrl-field"><label for="wt-to">To</label><%= date_field_tag :to, period.to, max: Date.current, id: "wt-to" %></div>
28
- <div class="hrl-form-actions"><button type="submit" class="hrl-btn">Show range</button></div>
23
+ <%# One picker on show: the date range stays folded unless it is what was asked for. %>
24
+ <%= tag.details open: params[:period] == "custom" do %>
25
+ <summary class="hrl-card__link">Pick your own dates</summary>
26
+ <%= form_with url: path.({}), method: :get, local: true, class: "hrl-row" do %>
27
+ <input type="hidden" name="period" value="custom">
28
+ <% (local_assigns[:keep] || {}).each do |k, v| %><input type="hidden" name="<%= k %>" value="<%= v %>"><% end %>
29
+ <div class="hrl-field"><label for="wt-from">From</label><%= date_field_tag :from, period.from, max: Date.current, id: "wt-from" %></div>
30
+ <div class="hrl-field"><label for="wt-to">To</label><%= date_field_tag :to, period.to, max: Date.current, id: "wt-to" %></div>
31
+ <div class="hrl-form-actions"><button type="submit" class="hrl-btn">Show range</button></div>
32
+ <% end %>
29
33
  <% end %>
30
34
  </section>
31
35
 
@@ -35,7 +39,8 @@
35
39
  <% office = result.by_mode["office"]; wfh = result.by_mode["wfh"]; field = result.by_mode["travelling"] + result.by_mode["outside_meeting"] %>
36
40
  <div class="hrl-kpis">
37
41
  <% [ [ "Attendance (check-in to check-out)", result.attendance ], [ "Working time (breaks included)", result.working + result.breaks ], [ "Of which breaks", result.breaks ],
38
- [ "In office", office ], [ "Work from home", wfh ], [ "Travel and client meetings", field ], [ "Out of office", result.other ] ].each do |kpi_label, secs| %>
42
+ [ "In office", office ], [ "Work from home", wfh ], [ "Travel and client meetings", field ],
43
+ [ "Meetings and busy", result.by_mode["meeting"] + result.by_mode["busy"] ], [ "Out of office", result.other ] ].each do |kpi_label, secs| %>
39
44
  <div class="hrl-kpi"><div class="hrl-kpi__value"><%= hrl_duration(secs) %></div><div class="hrl-kpi__label"><%= kpi_label %></div></div>
40
45
  <% end %>
41
46
  </div>
@@ -62,11 +67,12 @@
62
67
  <% if records.any? %>
63
68
  <section class="hrl-card">
64
69
  <h2 class="hrl-card__title">Check-ins</h2>
70
+ <% all_punches = HrLite::AttendancePunch.where(user_id: records.map(&:user_id).uniq, date: records.map(&:date)).order(:at, :id).group_by { |p| [ p.user_id, p.date ] } # one query, not one a day %>
65
71
  <% records.each do |r| %>
66
72
  <div class="hrl-feed__item hrl-small"><div class="hrl-feed__body">
67
73
  <% if names %><strong><%= names[r.user_id] %></strong> · <% end %>
68
74
  <%= r.date.strftime("%a %-d %b") %><% if r.work_mode_label %> · <%= r.work_mode_label %><% end %>
69
- <%= render "hr_lite/attendance/trail", record: r %>
75
+ <%= render "hr_lite/attendance/trail", record: r, punches: all_punches.fetch([ r.user_id, r.date ], []) %>
70
76
  </div></div>
71
77
  <% end %>
72
78
  </section>
@@ -117,6 +117,7 @@
117
117
  <%= javascript_include_tag "hr_lite/select_filter", defer: true %>
118
118
  <%= javascript_include_tag "hr_lite/search", defer: true %>
119
119
  <dialog class="hrl-search" data-hrl-search aria-label="Search">
120
+ <form method="dialog" class="hrl-search__close"><button class="hrl-btn" type="submit">Close</button></form>
120
121
  <form action="<%= hr_lite.search_path %>" method="get" role="search">
121
122
  <label class="hrl-sr-only" for="hrl-search-q">Search screens and people</label>
122
123
  <input id="hrl-search-q" class="hrl-input" type="search" name="q" placeholder="Search screens and people" autocomplete="off">
@@ -131,14 +131,17 @@ module HrLite
131
131
  # Rescued PER RECIPIENT. Wrapping the whole loop meant one bad address
132
132
  # or a single enqueue failure silently dropped everyone after it — on
133
133
  # a team-wide leave notice, most of the team.
134
- Array(email_to).compact.uniq.each do |user|
134
+ users = Array(email_to).compact.uniq
135
+ # One query for everyone's Busy/Meeting end, not one per recipient.
136
+ held_until = HrLite.config.office_geofence ? WorkStatus.do_not_disturb_for(users.map(&:id)) : {}
137
+ users.each do |user|
135
138
  next if user.email.blank?
136
139
 
137
140
  begin
138
141
  mail = EventMailer.event(to: user.email, subject: title, heading: title,
139
142
  body: body, lines: lines, path: path, link_url: link_url)
140
143
  # Busy or in a meeting: the email waits until that ends.
141
- held = WorkStatus.do_not_disturb_until(user.id) if HrLite.config.office_geofence
144
+ held = held_until[user.id]
142
145
  held ? mail.deliver_later(wait_until: held) : mail.deliver_later
143
146
  rescue => e
144
147
  Rails.logger.error("[hr_lite] event email failed: #{e.class}: #{e.message}")
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.54.0"
2
+ VERSION = "0.55.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.54.0
4
+ version: 0.55.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kshitiz sinha
@@ -171,6 +171,7 @@ files:
171
171
  - app/jobs/hr_lite/leave_year_rollover_job.rb
172
172
  - app/jobs/hr_lite/payroll_auto_approve_job.rb
173
173
  - app/jobs/hr_lite/payroll_auto_draft_job.rb
174
+ - app/jobs/hr_lite/payroll_compute_job.rb
174
175
  - app/jobs/hr_lite/slips_ready_job.rb
175
176
  - app/jobs/hr_lite/work_mode_expire_job.rb
176
177
  - app/mailers/hr_lite/application_mailer.rb