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 +4 -4
- data/CHANGELOG.md +29 -0
- data/app/assets/stylesheets/hr_lite/hr_lite.css +1 -1
- data/app/controllers/hr_lite/admin/employees_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/settings_controller.rb +2 -1
- data/app/jobs/hr_lite/attendance_close_job.rb +4 -3
- data/app/models/hr_lite/attendance_record.rb +21 -12
- data/app/models/hr_lite/attendance_slot.rb +0 -1
- data/app/models/hr_lite/employee_profile.rb +11 -0
- data/app/models/hr_lite/setting.rb +5 -0
- data/app/services/hr_lite/attendance_puncher.rb +21 -9
- data/app/views/hr_lite/admin/employees/_form.html.erb +5 -0
- data/app/views/hr_lite/admin/settings/edit.html.erb +19 -0
- data/app/views/hr_lite/attendance/_punch_card.html.erb +13 -6
- data/app/views/hr_lite/attendance/_slot.html.erb +1 -1
- data/app/views/hr_lite/id_cards/show.html.erb +2 -1
- data/db/migrate/20261015090000_add_working_day_to_hr_lite_settings.rb +8 -0
- data/db/migrate/20261015091000_add_rechecked_in_at_to_hr_lite_attendance_records.rb +5 -0
- data/db/migrate/20261015092000_add_hours_allowance_to_hr_lite_employee_profiles.rb +6 -0
- data/lib/hr_lite/configuration.rb +3 -3
- data/lib/hr_lite/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ecbd41290643cbba3981491be8aea1e69d0bd38e5dab9dc006c803cd254204a
|
|
4
|
+
data.tar.gz: 1c6222a8ba272cd6c9818c97d54f16dbef5dc69fb9466293ae89c5809947be1f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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.
|
|
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)}
|
|
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
|
-
#
|
|
55
|
-
#
|
|
56
|
-
def
|
|
57
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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:
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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"
|
|
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
|
-
<%
|
|
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
|
-
|
|
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::
|
|
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
|
-
|
|
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
|
|
@@ -65,9 +65,9 @@ module HrLite
|
|
|
65
65
|
@self_regularization = value
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
-
# nil = no hours rule.
|
|
69
|
-
#
|
|
70
|
-
# week under `week` hours (pro rata
|
|
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
|
data/lib/hr_lite/version.rb
CHANGED
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.
|
|
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
|