hr_lite 0.40.1 → 0.43.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21440aa07238881b5cd3634fe5ab6d0bc1f4a101ab64dfeab82fd61f2bdb3b40
4
- data.tar.gz: 17c0c09696da50c92f0512ec885981f8965ba493519a1d4194f692c1a28e020b
3
+ metadata.gz: 2ecbd41290643cbba3981491be8aea1e69d0bd38e5dab9dc006c803cd254204a
4
+ data.tar.gz: 1c6222a8ba272cd6c9818c97d54f16dbef5dc69fb9466293ae89c5809947be1f
5
5
  SHA512:
6
- metadata.gz: 4e04c65673ad94c2a6adbe6663c54f546066fb4d5b8dc4229c101eb4b2b2b171eab98e931d7668a69930379969d82dcbf657b6f22a647363bc9d8d16ecf8fae7
7
- data.tar.gz: dc39966df09fbf335a48bf658f773a4e38a827002011b28f5c87ddf82dceada302f43593be3294b726d13aa9199d7df6ff9f4ff91097d73b122205d74ab2edb7
6
+ metadata.gz: db24e808b285af051a738dc733fb092307edab3f2fb5aef17740140ed40071d32788773d2c1e80aa93152c78a2a6b6daee893df5022852295636734dfa64c297
7
+ data.tar.gz: 75da26b92e93d18e7da0f92a73b25df66d505aa5f6313717ef3169cbd004c997867d455fccca13590fd770bb66c029f8837bc5724b044d1168a3522d3df12a06
data/CHANGELOG.md CHANGED
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.43.0] - 2026-10-15
11
+
12
+ - Shorter day: HR can give one employee 30, 60 or 90 minutes off the working day, for example for a long commute. It's set on their profile, with a required reason. The reason is encrypted, kept out of the audit trail, and seen only by HR. Their day becomes the Settings day minus the allowance (8h30 − 90 min = 7h). That drives their "+30 min", "Short by", checkout due time, half-day line check and slot picker. They see "Your day 7h 00m" on their attendance card. This is separate from the late buffer, which still only delays the late clock.
13
+ - `EmployeeProfile.day_minutes_for(user_id)` is the one place that works out a person's day.
14
+ - Migration: `hours_allowance_minutes` and `hours_allowance_reason` on `hr_lite_employee_profiles`.
15
+
16
+ ## [0.42.1] - 2026-10-15
17
+
18
+ - "Check in again" works once a day, and only inside the hours HR sets in Settings (default 09:00–20:00). Once it has been used, or outside those hours, the card shows "Check in again is open once a day, 09:00–20:00." with no button, and the server refuses the punch with the same message. This replaces 0.41.0's "as many times as needed".
19
+ - Migration: `hr_lite_attendance_records.rechecked_in_at`, stamped on the re-check-in.
20
+
21
+ ## [0.42.0] - 2026-10-15
22
+
23
+ - HR sets the working day in Admin › Settings. The working day is check-in to check-out with the break included (default 510 min = 8h 30m). Settings also hold the half-day line (default 255 min = 4h 15m) and the hours when "Check in again" is open (default 09:00–20:00). Changes are audited.
24
+ - A short day is measured in hours worked, not by the clock. The checkout due time is the first check-in + the working day + that day's penalties, so someone who comes at 9:00 and leaves at 17:30 has done 8h30 and gets no "+30 min". Before, the due time was the slot start + 8h30, so an early start still counted as short.
25
+ - A day becomes a half day, with the notice, only when the hours worked are under the half-day line ("You worked 4.0h on 06 Jul, under 4h15 — marked half day"). Short of the working day but above the line: only the +30 min on the next working day. Before, anything under 8h was a half day.
26
+ - One working day for everyone: probation staff no longer need 9h.
27
+ - `config.work_hours` still turns the hours rules on (nil = off), and `week` is still read. `day` and `probation_day` are no longer read. `AttendanceSlot::DAY_MINUTES` is removed. The slot picker shows each check-out time from the Settings day.
28
+ - Migration: four columns on `hr_lite_settings`.
29
+
30
+ ## [0.41.0] - 2026-10-15
31
+
32
+ - Check out asks first: "Check out now? Your check-out is saved as soon as you press OK." The check-out is recorded only on OK; Cancel leaves you checked in. This covers both the day's Check out and "Check out yesterday's shift".
33
+ - Check in again: after checking out, the card offers "Check in again", as many times as needed in a day. It reopens the day. The first check-in time stands and the check-out is cleared until the next one. The gap shows as Offline on the work-mode timeline, so working time excludes it, and the new session takes this punch's work mode. Re-checking in does not apply the late-arrival rules again. "Update check-out" is gone: check in again, then check out.
34
+
35
+ ## [0.40.2] - 2026-10-15
36
+
37
+ - ID card: a long name no longer runs off the card. When someone has no name and their email stands in for it ("sageashish2714@gmail.com"), it breaks before the "@" onto a second line. Any other unbroken word breaks wherever it has to.
38
+
10
39
  ## [0.40.1] - 2026-10-15
11
40
 
12
41
  - Managers see their team's tabs. A Manager (leave and attendance at team scope) now gets Home › Company overview, Attendance › Team and Work modes, Leave › Team balances, Inbox (All, Leave requests, Comp-off, Attendance fixes, WFH, Home locations) and Admin › Reports. Those pages already let a manager in and show only their team's rows. The menu hid them unless you had the permission across the whole company or were leadership.
@@ -549,7 +549,7 @@ a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--
549
549
  .hrl-idcard__photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
550
550
  .hrl-photo-zoom { width: 100%; min-height: var(--hrl-target); accent-color: var(--hrl-accent); }
551
551
  .hrl-idcard__who { margin-top: calc(124 * var(--u)); text-align: center; padding: 0 calc(24 * var(--u)); }
552
- .hrl-idcard__name { font: 800 calc(25 * var(--u))/1.1 var(--hrl-card-display, var(--hrl-font)); letter-spacing: -.02em; text-wrap: balance; }
552
+ .hrl-idcard__name { font: 800 calc(25 * var(--u))/1.1 var(--hrl-card-display, var(--hrl-font)); letter-spacing: -.02em; text-wrap: balance; overflow-wrap: anywhere; }
553
553
  .hrl-idcard__role { margin-top: calc(6 * var(--u)); font: 500 calc(13 * var(--u))/1.3 var(--hrl-card-body, var(--hrl-font)); color: #334155; }
554
554
  .hrl-idcard__green { color: var(--green); }
555
555
  .hrl-idcard__blue { color: var(--blue); }
@@ -116,7 +116,7 @@ module HrLite
116
116
  :date_of_exit, :probation_until, :department, :work_location, :pan_number, :pf_uan, :esi_number,
117
117
  :bank_account_number, :bank_ifsc, :bank_name, :tax_regime, :declared_annual_deductions,
118
118
  :fy_opening_gross, :fy_opening_tds, :blood_group, :emergency_contact,
119
- :buffer_minutes, :buffer_reason, :buffer_note, :permanent_wfh, :field_staff, :office_location_id
119
+ :buffer_minutes, :buffer_reason, :buffer_note, :hours_allowance_minutes, :hours_allowance_reason, :permanent_wfh, :field_staff, :office_location_id
120
120
  )
121
121
  end
122
122
 
@@ -7,7 +7,8 @@ module HrLite
7
7
 
8
8
  def update
9
9
  @setting = Setting.instance
10
- if @setting.update(params.require(:setting).permit(:weekend_policy, :employee_code_prefix, :lunch_break_minutes, :short_break_minutes))
10
+ if @setting.update(params.require(:setting).permit(:weekend_policy, :employee_code_prefix, :lunch_break_minutes, :short_break_minutes,
11
+ :day_minutes, :half_day_below_minutes, :recheck_in_from, :recheck_in_until))
11
12
  redirect_to edit_admin_setting_path, notice: "Settings saved."
12
13
  else
13
14
  render :edit, status: :unprocessable_entity
@@ -2,7 +2,7 @@ module HrLite
2
2
  # Closes the working day that ended last: a run at 23:55 closes today, and a
3
3
  # retry or late run after midnight (or a morning schedule) closes yesterday.
4
4
  # A punch left open is checked out at the day's end and paid as a half day,
5
- # and so is a closed punch short of config.work_hours.
5
+ # and so is a closed punch under the half-day line HR sets in Settings.
6
6
  # Nobody punched in already reads as absent (DayStatus), so they are only
7
7
  # told. Leave days, and each person's weekends and office holidays, are left alone.
8
8
  class AttendanceCloseJob < ApplicationJob
@@ -22,10 +22,11 @@ module HrLite
22
22
  record.update!(check_out_at: date.end_of_day, status: "half_day",
23
23
  regularization_note: AttendanceRecord::AUTO_CHECKOUT_NOTE)
24
24
  tell(user, date, "attendance.missed_check_out", "You didn't check out on #{day} — marked half day")
25
- elsif !record.regularized? && record.status == "present" && record.short?
25
+ elsif !record.regularized? && record.status == "present" && record.below_half_day?
26
26
  record.update!(status: "half_day", regularization_note: AttendanceRecord::SHORT_DAY_NOTE)
27
+ line = Setting.instance.half_day_below_minutes
27
28
  tell(user, date, "attendance.short_day",
28
- "You worked #{(record.worked_duration / 3600.0).round(1)} of #{record.required_hours}h on #{day} — marked half day")
29
+ "You worked #{(record.worked_duration / 3600.0).round(1)}h on #{day}, under #{line / 60}h#{format('%02d', line % 60)} — marked half day")
29
30
  end
30
31
  penalize_short_day(record) if record&.check_out_at
31
32
  end
@@ -51,27 +51,36 @@ module HrLite
51
51
  check_out_at - check_in_at
52
52
  end
53
53
 
54
- # config.work_hours: probation days need more hours. Nil when the rule is
55
- # off, and on Saturdays (a working Saturday has no hours rule).
56
- def required_hours
57
- rule = HrLite.config.work_hours
58
- return if rule.nil? || date.saturday?
59
-
60
- EmployeeProfile.find_by(user_id: user_id)&.on_probation?(date) ? rule[:probation_day] : rule[:day]
54
+ # The working day HR sets in Settings, less the person's allowance. Nil when
55
+ # the host turns hours rules off (config.work_hours), and on Saturdays.
56
+ def required_minutes
57
+ EmployeeProfile.day_minutes_for(user_id) unless HrLite.config.work_hours.nil? || date.saturday?
61
58
  end
62
59
 
60
+ # Hours worked, not the clock: an early start counts.
63
61
  def short?
64
- hours = required_hours
65
- !!(hours && worked_duration && worked_duration < hours.hours)
62
+ worked_under?(required_minutes)
63
+ end
64
+
65
+ def below_half_day?
66
+ worked_under?(required_minutes && Setting.instance.half_day_below_minutes)
66
67
  end
67
68
 
68
- # config.attendance_slots: slot start + 8h30 + the day's active penalties.
69
+ # config.attendance_slots: first check-in + the working day + the day's active penalties.
69
70
  def required_check_out_at
70
71
  return unless HrLite.config.attendance_slots && check_in_at
71
72
 
72
73
  extra = AttendancePenalty.active.where(user_id: user_id, date: date).sum(:minutes)
73
- date.in_time_zone(HrLite.config.time_zone) +
74
- (AttendanceSlot.start_minute_for(user_id, date) + AttendanceSlot::DAY_MINUTES + extra).minutes
74
+ check_in_at + (EmployeeProfile.day_minutes_for(user_id) + extra).minutes
75
+ end
76
+
77
+ def worked_under?(minutes) = !!(minutes && worked_duration && worked_duration < minutes.minutes)
78
+
79
+ # Check in again after checking out: once a day, inside the hours HR sets in Settings.
80
+ def recheck_in_open?(at = Time.current)
81
+ s = Setting.instance
82
+ now = at.in_time_zone(HrLite.config.time_zone).strftime("%H:%M")
83
+ check_out_at.present? && rechecked_in_at.nil? && now >= s.recheck_in_from && now < s.recheck_in_until
75
84
  end
76
85
 
77
86
  # Metres between where the day was checked in and checked out.
@@ -7,7 +7,6 @@ module HrLite
7
7
  START_MINUTES = [ 540, 570, 600, 630 ].freeze # 09:00 09:30 10:00 10:30
8
8
  DEFAULT_START_MINUTE = 600
9
9
  PERIODS = { "day" => "Today", "week" => "This week", "month" => "This month" }.freeze
10
- DAY_MINUTES = 510 # 8h30m from slot to checkout
11
10
  GRACE_MINUTES = 5
12
11
  STEP_MINUTES = 30
13
12
  CARRY_AFTER_MINUTE = 660 # 11:00 — +1h on the next 2 working days
@@ -33,6 +33,17 @@ module HrLite
33
33
  validates :buffer_reason, inclusion: { in: BUFFER_REASONS.keys }, if: :buffer_minutes
34
34
  validates :buffer_note, presence: true, if: -> { buffer_minutes && buffer_reason == "other" }
35
35
 
36
+ # HR-granted shorter day (e.g. a long commute): minutes off the Settings
37
+ # day, so 90 off 8h30 is 7h. Only HR sees the reason (encrypted).
38
+ HOURS_ALLOWANCE_MINUTES = [ 30, 60, 90 ].freeze
39
+ encrypts :hours_allowance_reason
40
+ normalizes :hours_allowance_minutes, with: ->(v) { v.presence }
41
+ validates :hours_allowance_minutes, inclusion: { in: HOURS_ALLOWANCE_MINUTES }, allow_nil: true
42
+ validates :hours_allowance_reason, presence: true, if: :hours_allowance_minutes
43
+
44
+ # One person's working day: HR's day in Settings less their allowance.
45
+ def self.day_minutes_for(user_id) = Setting.instance.day_minutes - where(user_id: user_id).pick(:hours_allowance_minutes).to_i
46
+
36
47
  TAX_REGIMES = %w[new old].freeze
37
48
  BLOOD_GROUPS = %w[A+ A- B+ B- AB+ AB- O+ O-].freeze
38
49
  normalizes :blood_group, with: ->(v) { v.presence } # the "Not set" chip posts ""
@@ -8,6 +8,11 @@ module HrLite
8
8
  validates :weekend_policy, inclusion: { in: WEEKEND_POLICIES }
9
9
  validates :lunch_break_minutes, numericality: { only_integer: true, in: 5..120 }
10
10
  validates :short_break_minutes, numericality: { only_integer: true, in: 1..60 }
11
+ # The working day is check-in to check-out, break included (8h30 = 8h + 30m).
12
+ validates :day_minutes, numericality: { only_integer: true, in: 240..720 }
13
+ validates :half_day_below_minutes, numericality: { only_integer: true, greater_than: 0, less_than: :day_minutes }
14
+ validates :recheck_in_from, :recheck_in_until, format: { with: /\A([01]\d|2[0-3]):[0-5]\d\z/, message: "must be HH:MM" }
15
+ validate { errors.add(:recheck_in_until, "must be after the start") if recheck_in_from.to_s >= recheck_in_until.to_s }
11
16
  validates :employee_code_prefix, presence: true,
12
17
  format: { with: /\A[A-Za-z]{1,10}-?\z/, message: "letters only (max 10), optionally ending in -" }
13
18
 
@@ -61,13 +61,25 @@ module HrLite
61
61
  record.with_lock do
62
62
  case @kind
63
63
  when :check_in
64
- return Result.new(record: record, error: already_in_message(record)) if record.check_in_at.present?
65
-
66
- stamp(record, :check_in)
67
- record.work_mode = @mode
68
- record.work_location_id = @location&.id
69
- record.wfh_request_id = @wfh&.id if @mode == "wfh"
70
- record.add_flag!("WFH request pending HR approval: #{@wfh.reason}") if @wfh&.pending?
64
+ if (reopen = record.check_out_at.present?)
65
+ # Checking in again reopens the day: the first check-in stands, the
66
+ # gap stays Offline on the work-mode timeline.
67
+ unless record.recheck_in_open?
68
+ s = Setting.instance
69
+ return Result.new(record: record, error: "Check in again is open once a day, #{s.recheck_in_from}–#{s.recheck_in_until}.")
70
+ end
71
+
72
+ record.assign_attributes(check_out_at: nil, check_out_lat: nil, check_out_lng: nil, check_out_accuracy_m: nil,
73
+ check_out_place: nil, check_out_home_m: nil, check_out_work_location_id: nil, rechecked_in_at: Time.current)
74
+ else
75
+ return Result.new(record: record, error: already_in_message(record)) if record.check_in_at.present?
76
+
77
+ stamp(record, :check_in)
78
+ record.work_mode = @mode
79
+ record.work_location_id = @location&.id
80
+ record.wfh_request_id = @wfh&.id if @mode == "wfh"
81
+ record.add_flag!("WFH request pending HR approval: #{@wfh.reason}") if @wfh&.pending?
82
+ end
71
83
  when :check_out
72
84
  return Result.new(record: record, error: "Check in first.") if record.check_in_at.blank?
73
85
 
@@ -79,7 +91,7 @@ module HrLite
79
91
 
80
92
  apply_geo_flags(record)
81
93
  record.save!
82
- apply_slot_rules(record) if @kind == :check_in && HrLite.config.attendance_slots
94
+ apply_slot_rules(record) if @kind == :check_in && !reopen && HrLite.config.attendance_slots
83
95
  track_work_mode(record) if HrLite.config.office_geofence
84
96
  end
85
97
  notify_if_flagged(record)
@@ -193,7 +205,7 @@ module HrLite
193
205
  # opens Offline. The timeline keeps every earlier row.
194
206
  def track_work_mode(record)
195
207
  if @kind == :check_in
196
- WorkModeEvent.start!(user_id: @user.id, mode: record.work_mode || "office", source: "system",
208
+ WorkModeEvent.start!(user_id: @user.id, mode: @mode || record.work_mode || "office", source: "system",
197
209
  attendance_record: record, wfh_request_id: record.wfh_request_id,
198
210
  work_location_id: record.work_location_id, title: @field[:purpose], destination: @field[:place])
199
211
  else
@@ -90,6 +90,11 @@
90
90
  </div>
91
91
  <div class="hrl-field"><%= f.label :buffer_note, "Buffer note (HR only; required for Other)" %><%= f.text_area :buffer_note, rows: 2 %></div>
92
92
  <% end %>
93
+ <div class="hrl-field">
94
+ <%= f.label :hours_allowance_minutes, "Shorter day (e.g. a long commute; lowers the hours they must work)" %>
95
+ <%= f.select :hours_allowance_minutes, HrLite::EmployeeProfile::HOURS_ALLOWANCE_MINUTES.map { |m| [ "#{m} min less", m ] }, include_blank: "None" %>
96
+ </div>
97
+ <div class="hrl-field"><%= f.label :hours_allowance_reason, "Shorter day reason (HR only; required)" %><%= f.text_area :hours_allowance_reason, rows: 2 %></div>
93
98
  <% if HrLite.config.office_geofence %>
94
99
  <div class="hrl-field hrl-field--check"><%= f.check_box :permanent_wfh %><%= f.label :permanent_wfh, "Permanent work from home (every working day counts as approved WFH; check-in still within 500 m of their approved home location)" %></div>
95
100
  <div class="hrl-field hrl-field--check"><%= f.check_box :field_staff %><%= f.label :field_staff, "Field staff (e.g. sales: may check in from anywhere as Travelling or Client meeting, with purpose and place)" %></div>
@@ -20,6 +20,25 @@
20
20
  <%= f.label :short_break_minutes, "Short break limit (minutes)" %>
21
21
  <%= f.number_field :short_break_minutes, min: 1, max: 60 %>
22
22
  </div>
23
+ <div class="hrl-field">
24
+ <%= f.label :day_minutes, "Working day (minutes, break included)" %>
25
+ <%= f.number_field :day_minutes, min: 240, max: 720, step: 15 %>
26
+ <p class="hrl-hint">Check-in to check-out. 510 = 8h 30m (8h work + 30m break). Leaving earlier adds 30 min to the next working day.</p>
27
+ </div>
28
+ <div class="hrl-field">
29
+ <%= f.label :half_day_below_minutes, "Half day below (minutes worked)" %>
30
+ <%= f.number_field :half_day_below_minutes, min: 15, step: 15 %>
31
+ <p class="hrl-hint">Working fewer minutes than this marks the day a half day and tells the person. 255 = 4h 15m.</p>
32
+ </div>
33
+ <div class="hrl-field">
34
+ <%= f.label :recheck_in_from, "Check in again: from" %>
35
+ <%= f.time_field :recheck_in_from %>
36
+ </div>
37
+ <div class="hrl-field">
38
+ <%= f.label :recheck_in_until, "Check in again: until" %>
39
+ <%= f.time_field :recheck_in_until %>
40
+ <p class="hrl-hint">After checking out, a person can check in again once a day inside these hours.</p>
41
+ </div>
23
42
  <div class="hrl-field">
24
43
  <%= f.label :employee_code_prefix, "Employee code prefix" %>
25
44
  <%= f.text_field :employee_code_prefix %>
@@ -1,4 +1,6 @@
1
1
  <% record = HrLite::AttendanceRecord.find_by(user_id: hr_current_user.id, date: Date.current) %>
2
+ <%# Checking out by mistake was common; the OK is the deliberate step. Checking in again reopens the day. %>
3
+ <% checkout_confirm = "Check out now? Your check-out is saved as soon as you press OK." %>
2
4
  <%# A shift that runs past midnight leaves yesterday's record open.
3
5
  AttendancePuncher already closes it, but this card looked only at today,
4
6
  so after midnight it offered "Check in" and the night's punch never
@@ -21,6 +23,9 @@
21
23
  <% if record&.work_mode_label %>
22
24
  <span class="hrl-badge hrl-badge--ok"><%= record.work_mode_label %></span>
23
25
  <% end %>
26
+ <% if HrLite::EmployeeProfile.where(user_id: hr_current_user.id).pick(:hours_allowance_minutes) %>
27
+ <span class="hrl-badge hrl-badge--muted">Your day <%= hrl_duration(HrLite::EmployeeProfile.day_minutes_for(hr_current_user.id) * 60) %></span>
28
+ <% end %>
24
29
  <% if record&.check_in_place == "home" && record.work_location %>
25
30
  <span class="hrl-badge hrl-badge--muted"><%= record.work_location.name %> · <%= HrLite::WorkLocation::FENCE_M %> m</span>
26
31
  <% end %>
@@ -46,7 +51,7 @@
46
51
 
47
52
  <% if open_yesterday %>
48
53
  <%= form_with url: hr_lite.check_out_attendance_path, method: :post, local: true,
49
- html: { data: { "hrl-geo-punch": true, "hrl-geo-required": HrLite.config.require_punch_location.presence } } do %>
54
+ html: { data: { "hrl-geo-punch": true, "hrl-geo-required": HrLite.config.require_punch_location.presence, "turbo-confirm": checkout_confirm } } do %>
50
55
  <input type="hidden" name="lat"><input type="hidden" name="lng">
51
56
  <input type="hidden" name="accuracy_m"><input type="hidden" name="geo_status">
52
57
  <button type="submit" class="hrl-btn hrl-btn--big hrl-btn--block">
@@ -54,7 +59,7 @@
54
59
  </button>
55
60
  <p class="hrl-small hrl-muted" data-hrl-geo-status role="status" aria-live="polite"></p>
56
61
  <% end %>
57
- <% elsif record&.check_in_at.blank? %>
62
+ <% elsif record&.check_in_at.blank? || record.recheck_in_open? %>
58
63
  <%= form_with url: hr_lite.check_in_attendance_path, method: :post, local: true,
59
64
  html: { data: { "hrl-geo-punch": true, "hrl-geo-required": HrLite.config.require_punch_location.presence } } do %>
60
65
  <input type="hidden" name="lat"><input type="hidden" name="lng">
@@ -82,22 +87,24 @@
82
87
  <label class="hrl-field--check"><input type="radio" name="place" value="travel" required> No, I am travelling</label>
83
88
  </fieldset>
84
89
  <% end %>
85
- <button type="submit" class="hrl-btn hrl-btn--primary hrl-btn--big hrl-btn--block">Check in</button>
90
+ <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>
86
91
  <p class="hrl-small hrl-muted" data-hrl-geo-status role="status" aria-live="polite"></p>
87
92
  <% end %>
88
- <% else %>
93
+ <% elsif record.check_out_at.nil? %>
89
94
  <%= form_with url: hr_lite.check_out_attendance_path, method: :post, local: true,
90
- html: { data: { "hrl-geo-punch": true, "hrl-geo-required": HrLite.config.require_punch_location.presence } } do %>
95
+ html: { data: { "hrl-geo-punch": true, "hrl-geo-required": HrLite.config.require_punch_location.presence, "turbo-confirm": checkout_confirm } } do %>
91
96
  <input type="hidden" name="lat"><input type="hidden" name="lng">
92
97
  <input type="hidden" name="accuracy_m"><input type="hidden" name="geo_status">
93
98
  <% if flash[:punch_offer] == "travel" %>
94
99
  <div class="hrl-field"><label class="hrl-field--check"><input type="checkbox" name="place" value="travel" required> I am travelling — check out here</label></div>
95
100
  <% end %>
96
101
  <button type="submit" class="hrl-btn hrl-btn--big hrl-btn--block">
97
- <%= record.check_out_at ? "Update check-out" : "Check out" %>
102
+ Check out
98
103
  </button>
99
104
  <p class="hrl-small hrl-muted" data-hrl-geo-status role="status" aria-live="polite"></p>
100
105
  <% end %>
106
+ <% else %>
107
+ <p class="hrl-small hrl-muted">Check in again is open once a day, <%= HrLite::Setting.instance.recheck_in_from %>–<%= HrLite::Setting.instance.recheck_in_until %>.</p>
101
108
  <% end %>
102
109
 
103
110
  <% if local_assigns[:attention].present? %><p class="hrl-small hrl-muted hrl-mt">Needs a fix</p><% end %>
@@ -21,7 +21,7 @@
21
21
  <%= form_with url: hr_lite.slot_attendance_path, method: :post, local: true, class: "hrl-mt" do %>
22
22
  <div class="hrl-field">
23
23
  <label for="hrl-slot-start">Check-in slot</label>
24
- <%= select_tag :start_minute, options_for_select(HrLite::AttendanceSlot::START_MINUTES.map { |m| [ "#{HrLite::AttendanceSlot.label(m)} – out #{HrLite::AttendanceSlot.label(m + HrLite::AttendanceSlot::DAY_MINUTES)}", m ] }, start), id: "hrl-slot-start" %>
24
+ <%= select_tag :start_minute, options_for_select(HrLite::AttendanceSlot::START_MINUTES.map { |m| [ "#{HrLite::AttendanceSlot.label(m)} – out #{HrLite::AttendanceSlot.label(m + HrLite::EmployeeProfile.day_minutes_for(hr_current_user.id))}", m ] }, start), id: "hrl-slot-start" %>
25
25
  </div>
26
26
  <div class="hrl-field">
27
27
  <label for="hrl-slot-period">For</label>
@@ -30,7 +30,8 @@
30
30
  <% end %>
31
31
  </div>
32
32
  <div class="hrl-idcard__who">
33
- <div class="hrl-idcard__name"><span class="hrl-idcard__green"><%= first %></span> <span class="hrl-idcard__blue"><%= rest %></span></div>
33
+ <%# An email standing in for the name breaks before its "@", else anywhere (CSS). %>
34
+ <div class="hrl-idcard__name"><span class="hrl-idcard__green"><%= safe_join(first.to_s.split("@", 2), tag.wbr + "@") %></span> <span class="hrl-idcard__blue"><%= rest %></span></div>
34
35
  <div class="hrl-idcard__role"><%= @profile.designation %></div>
35
36
  </div>
36
37
  <dl class="hrl-idcard__facts">
@@ -0,0 +1,8 @@
1
+ class AddWorkingDayToHrLiteSettings < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :hr_lite_settings, :day_minutes, :integer, null: false, default: 510
4
+ add_column :hr_lite_settings, :half_day_below_minutes, :integer, null: false, default: 255
5
+ add_column :hr_lite_settings, :recheck_in_from, :string, null: false, default: "09:00"
6
+ add_column :hr_lite_settings, :recheck_in_until, :string, null: false, default: "20:00"
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ class AddRecheckedInAtToHrLiteAttendanceRecords < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :hr_lite_attendance_records, :rechecked_in_at, :datetime
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddHoursAllowanceToHrLiteEmployeeProfiles < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :hr_lite_employee_profiles, :hours_allowance_minutes, :integer
4
+ add_column :hr_lite_employee_profiles, :hours_allowance_reason, :text
5
+ end
6
+ end
@@ -65,9 +65,9 @@ module HrLite
65
65
  @self_regularization = value
66
66
  end
67
67
 
68
- # nil = no hours rule. { day: 8, probation_day: 9, week: 40 } = a closed
69
- # Mon–Fri punch short of its day's hours is closed as a half day, and a
70
- # week under `week` hours (pro rata for holidays and leave) is flagged.
68
+ # nil = no hours rule. Set = the day rules run on the working day and the
69
+ # half-day line HR sets in Settings (`day` and `probation_day` here are no
70
+ # longer read), and a week under `week` hours (pro rata) is flagged.
71
71
  def work_hours=(value)
72
72
  ok = value.nil? || (value.is_a?(Hash) && %i[day probation_day week].all? { |k| value[k].is_a?(Numeric) && value[k].positive? })
73
73
  raise ArgumentError, "work_hours must be nil or { day:, probation_day:, week: } (positive numbers), got #{value.inspect}" unless ok
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.40.1"
2
+ VERSION = "0.43.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.40.1
4
+ version: 0.43.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kshitiz sinha
@@ -486,6 +486,9 @@ files:
486
486
  - db/migrate/20261013140000_add_calendar_and_crm_to_hr_lite_work_mode_events.rb
487
487
  - db/migrate/20261013140100_add_custom_status_to_hr_lite_work_mode_events.rb
488
488
  - db/migrate/20261014031000_add_punch_places_to_hr_lite_attendance_records.rb
489
+ - db/migrate/20261015090000_add_working_day_to_hr_lite_settings.rb
490
+ - db/migrate/20261015091000_add_rechecked_in_at_to_hr_lite_attendance_records.rb
491
+ - db/migrate/20261015092000_add_hours_allowance_to_hr_lite_employee_profiles.rb
489
492
  - lib/generators/hr_lite/install/install_generator.rb
490
493
  - lib/generators/hr_lite/install/templates/AFTER_INSTALL
491
494
  - lib/generators/hr_lite/install/templates/initializer.rb