hr_lite 0.35.1 → 0.36.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: b89cc7943db74414a4d788a6455c972cbc114f786a1f4f9441908dba5fe9a0a4
4
- data.tar.gz: 7a80d0e103a2dab3720ba5b3f3abbc861e1b0133d85edd20035314667b325928
3
+ metadata.gz: 91b052360d5658195be8a557e080ddcaea5660de77265ff6cb4d030b879f23a8
4
+ data.tar.gz: 32a1e1624df566187143af64b995b0af4a2db0c44463ff7600d0f6d7ed0f2e32
5
5
  SHA512:
6
- metadata.gz: 5b0d4fa99d60e1c3b4d15275ab7309032922c384d64aaa9af950980427ade931dc91c369f78b8870708f04b1284cedbe89c990a9705a4b28c22fd74a0ffa7ca9
7
- data.tar.gz: 17f0cacac2684fd53c56d2faffb8846531529e3e97f723cd4d9845eb78ed1bc5d687dfd1116b2aeda08dc3c30faf36cfdd1e5a42b976fd3593595817330c15b6
6
+ metadata.gz: 0caced16fcf281925ba4ec901631dcffcd20e07aad5124781c67751298f1dfde737eb9ca60eaf4f3c100d47129a76a83bfb2ff93bd72d21db6de2834319dbc57
7
+ data.tar.gz: 8002305bba4b795098b2499a6765809a2a1aebf3c281fe715e7fc72f349ef3630cc653f2abbda98c32d03df4f5c223cbd4bf17e4ddb8b8404085aaa080a80f40
data/CHANGELOG.md CHANGED
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.36.0] - 2026-10-13
11
+
12
+ - Pick how long a mode lasts: Busy, Meeting and Out of office offer 15 min, 30 min, 1 h, 2 h, 4 h or 1 day (30 min preselected); Travelling and Client meeting also offer "Until I change it" (the default). Lunch and short break offer only lengths within their company limit — lunch 15 or 30 min by default — and one tap still takes the full limit; a longer break is refused. Every mode ends on its own at the picked time; the longest is a day. The choices are real radio buttons shown as 44px chips with 8px gaps.
13
+
10
14
  ## [0.35.1] - 2026-10-13
11
15
 
12
16
  - Spacing: forms, buttons and expandable sections stacked in a card no longer touch (only form fields carried spacing, so "Set a status" sat on "Change my mode", "Save status" on "Check in", "Out of office" on "Check out"). One card-level rule gives them a 12px gap. The emoji grid, lead picker, check-in badges, punch-card links (now wrapping as whole items, no dangling dots) and the admin month's In/Out map links all keep at least the 8px gap between touch targets. Measured at 360 px and 1280 px across Home, Attendance, WFH, WFH locations, My work time, Team and the HR work-mode, WFH, locations, work-time, attendance, holidays and employee screens: no stacked targets closer than 8px, no horizontal scroll.
@@ -43,7 +43,7 @@ module HrLite
43
43
  raise ActiveRecord::RecordNotFound if employee.id == hr_current_user.id
44
44
 
45
45
  result = WorkModeSwitch.call(user: employee, mode: params[:mode], source: "hr", actor: hr_current_user,
46
- params: params.permit(:title, :client, :destination, :reason, :minutes, :until, :category, :crm_ref))
46
+ params: params.permit(:title, :client, :destination, :reason, :minutes, :until, :category, :crm_ref, :duration))
47
47
  flash_for = result.ok? ? { notice: "#{hr_display_name(employee)} is now: #{result.event.label}." } : { alert: result.error }
48
48
  redirect_back fallback_location: admin_attendance_path(employee.id), **flash_for
49
49
  end
@@ -5,7 +5,7 @@ module HrLite
5
5
 
6
6
  def create
7
7
  result = WorkModeSwitch.call(user: hr_current_user, mode: params[:mode], source: "employee", actor: hr_current_user,
8
- params: params.permit(:title, :client, :destination, :reason, :minutes, :until, :category, :crm_ref))
8
+ params: params.permit(:title, :client, :destination, :reason, :minutes, :until, :category, :crm_ref, :duration))
9
9
  if result.ok?
10
10
  redirect_to attendance_path, notice: "You are now: #{result.event.label}."
11
11
  else
@@ -13,6 +13,17 @@ module HrLite
13
13
 
14
14
  def self.call(...) = new(...).call
15
15
 
16
+ # How long a mode lasts, picked per use (owner, 2026-10-13): 15 min up to a day.
17
+ DURATIONS = { "15" => 15.minutes, "30" => 30.minutes, "60" => 1.hour, "120" => 2.hours,
18
+ "240" => 4.hours, "day" => 1.day }.freeze
19
+ DURATION_LABELS = { "15" => "15 min", "30" => "30 min", "60" => "1 h", "120" => "2 h", "240" => "4 h", "day" => "1 day" }.freeze
20
+
21
+ # The lengths offered for a mode: breaks never past their company limit.
22
+ def self.duration_choices(mode)
23
+ cap = { "lunch" => Setting.instance.lunch_break_minutes, "short_break" => Setting.instance.short_break_minutes }[mode.to_s]
24
+ cap ? DURATIONS.keys.select { |k| DURATIONS[k] <= cap.minutes } : DURATIONS.keys
25
+ end
26
+
16
27
  CLEAR_AFTER = { "30" => 30.minutes, "60" => 1.hour, "240" => 4.hours, "today" => :today, "never" => nil }.freeze
17
28
 
18
29
  # A person's own status, Slack-style: works with or without a check-in
@@ -120,10 +131,18 @@ module HrLite
120
131
  day = @now.in_time_zone(HrLite.config.time_zone).all_day
121
132
  return "You have already taken lunch today." if WorkModeEvent.where(user_id: @user.id, mode: "lunch", started_at: day).exists?
122
133
 
123
- base.merge(ends_at: @now + Setting.instance.lunch_break_minutes.minutes)
134
+ capped_break(Setting.instance.lunch_break_minutes)
124
135
  end
125
136
 
126
- def short_break_attrs = base.merge(ends_at: @now + Setting.instance.short_break_minutes.minutes)
137
+ def short_break_attrs = capped_break(Setting.instance.short_break_minutes)
138
+
139
+ # A break runs the picked length (never past the limit), or the full limit.
140
+ def capped_break(limit)
141
+ return base.merge(ends_at: @now + limit.minutes) unless @p[:duration]
142
+ return "That break length is over the #{limit}-minute limit." unless self.class.duration_choices(@mode).include?(@p[:duration])
143
+
144
+ base.merge(ends_at: @now + DURATIONS[@p[:duration]])
145
+ end
127
146
 
128
147
  def parse_until
129
148
  Time.zone.parse("#{@now.to_date} #{@p[:until]}") if @p[:until]
@@ -131,12 +150,18 @@ module HrLite
131
150
  nil
132
151
  end
133
152
 
134
- # minutes (30/60/…) or until "HH:MM" today; must be in the future, today.
153
+ # A picked length (15 min … 1 day), or minutes / an "HH:MM" end time;
154
+ # always in the future and at most a day away.
135
155
  def timed(attrs, required: true)
136
- ends_at = @p[:minutes] ? @now + @p[:minutes].to_i.minutes : parse_until
156
+ return "Pick how long from the choices." if @p[:duration] && !DURATIONS.key?(@p[:duration])
157
+
158
+ ends_at = if @p[:duration] then @now + DURATIONS[@p[:duration]]
159
+ elsif @p[:minutes] then @now + @p[:minutes].to_i.minutes
160
+ else parse_until
161
+ end
137
162
  return attrs unless ends_at || required
138
- return "Pick when it ends." unless ends_at
139
- return "The end time must be later today." unless ends_at > @now && ends_at <= @now.end_of_day
163
+ return "Pick how long." unless ends_at
164
+ return "The end time must be later than now, and at most a day away." unless ends_at > @now && ends_at <= @now + 1.day
140
165
 
141
166
  attrs.merge(ends_at: ends_at)
142
167
  end
@@ -0,0 +1,16 @@
1
+ <%# How long a mode lasts: real radios styled as chips (selected state exposed).
2
+ locals: mode, id, optional (adds "Until I change it", the default) %>
3
+ <% choices = HrLite::WorkModeSwitch.duration_choices(mode)
4
+ optional = local_assigns[:optional]
5
+ default = optional ? "" : (choices.include?("30") ? "30" : choices.last) %>
6
+ <fieldset class="hrl-field">
7
+ <legend class="hrl-small"><strong>How long</strong></legend>
8
+ <div class="hrl-chiprow" role="radiogroup" aria-label="How long">
9
+ <% if optional %>
10
+ <label><input type="radio" name="duration" value="" <%= "checked" if default == "" %>><span>Until I change it</span></label>
11
+ <% end %>
12
+ <% choices.each do |key| %>
13
+ <label><input type="radio" name="duration" id="<%= id %>-<%= key %>" value="<%= key %>" <%= "checked" if key == default %>><span><%= HrLite::WorkModeSwitch::DURATION_LABELS[key] %></span></label>
14
+ <% end %>
15
+ </div>
16
+ </fieldset>
@@ -22,22 +22,37 @@
22
22
  <details>
23
23
  <summary class="hrl-btn hrl-btn--block">Change my mode</summary>
24
24
  <div class="hrl-mode-switch">
25
- <% %w[lunch short_break office].each do |mode| %>
26
- <%= button_to hr_lite.work_mode_path, params: { mode: mode }, method: :post, class: "hrl-btn hrl-btn--block" do %>
27
- <%= hrl_mode_icon(HrLite::WorkStatus::Entry.new(mode: mode, label: HrLite::WorkModeEvent.label(mode))) %>
28
- <%= HrLite::WorkModeEvent.label(mode) %>
29
- <% end %>
25
+ <%= button_to hr_lite.work_mode_path, params: { mode: "office" }, method: :post, class: "hrl-btn hrl-btn--block" do %>
26
+ <%= hrl_mode_icon(HrLite::WorkStatus::Entry.new(mode: "office", label: HrLite::WorkModeEvent.label("office"))) %>
27
+ <%= HrLite::WorkModeEvent.label("office") %>
30
28
  <% end %>
31
29
  <% if HrLite::WfhRequest.approved_for(hr_current_user.id, Date.current) || HrLite::EmployeeProfile.where(user_id: hr_current_user.id).pick(:permanent_wfh) %>
32
30
  <%= button_to "Work from home", hr_lite.work_mode_path, params: { mode: "wfh" }, method: :post, class: "hrl-btn hrl-btn--block" %>
33
31
  <% end %>
32
+ <%# Breaks: one tap for the full limit, or pick a shorter length when the limit allows a choice. %>
33
+ <% %w[lunch short_break].each do |mode| %>
34
+ <% if HrLite::WorkModeSwitch.duration_choices(mode).size > 1 %>
35
+ <details>
36
+ <summary class="hrl-btn hrl-btn--block"><%= HrLite::WorkModeEvent.label(mode) %></summary>
37
+ <%= form_with url: hr_lite.work_mode_path, method: :post, local: true do %>
38
+ <input type="hidden" name="mode" value="<%= mode %>">
39
+ <%= render "hr_lite/attendance/duration_chips", mode: mode, id: "dur-#{mode}" %>
40
+ <button type="submit" class="hrl-btn hrl-btn--primary">Start <%= HrLite::WorkModeEvent.label(mode).downcase %></button>
41
+ <% end %>
42
+ </details>
43
+ <% else %>
44
+ <%= button_to hr_lite.work_mode_path, params: { mode: mode }, method: :post, class: "hrl-btn hrl-btn--block" do %>
45
+ <%= hrl_mode_icon(HrLite::WorkStatus::Entry.new(mode: mode, label: HrLite::WorkModeEvent.label(mode))) %>
46
+ <%= HrLite::WorkModeEvent.label(mode) %>
47
+ <% end %>
48
+ <% end %>
49
+ <% end %>
34
50
 
35
51
  <details>
36
52
  <summary class="hrl-btn hrl-btn--block">Busy / focus</summary>
37
53
  <%= form_with url: hr_lite.work_mode_path, method: :post, local: true do %>
38
54
  <input type="hidden" name="mode" value="busy">
39
- <div class="hrl-field"><label for="busy-minutes">For</label>
40
- <%= select_tag :minutes, options_for_select([ [ "30 minutes", 30 ], [ "1 hour", 60 ], [ "2 hours", 120 ] ]), id: "busy-minutes" %></div>
55
+ <%= render "hr_lite/attendance/duration_chips", mode: "busy", id: "dur-busy" %>
41
56
  <button type="submit" class="hrl-btn hrl-btn--primary">Set busy</button>
42
57
  <% end %>
43
58
  </details>
@@ -48,7 +63,7 @@
48
63
  <div class="hrl-field"><label for="mt-title">Meeting</label><input class="hrl-input" id="mt-title" name="title" maxlength="120" required></div>
49
64
  <div class="hrl-field"><label for="mt-client">Client (optional)</label><input class="hrl-input" id="mt-client" name="client" maxlength="120"></div>
50
65
  <%= render "hr_lite/attendance/crm_picker", id: "mt-crm" %>
51
- <div class="hrl-field"><label for="mt-until">Ends at</label><input class="hrl-input" type="time" id="mt-until" name="until" required></div>
66
+ <%= render "hr_lite/attendance/duration_chips", mode: "meeting", id: "dur-meeting" %>
52
67
  <button type="submit" class="hrl-btn hrl-btn--primary">Start meeting</button>
53
68
  <% end %>
54
69
  </details>
@@ -60,7 +75,7 @@
60
75
  <div class="hrl-field"><label for="tr-why">Purpose</label><input class="hrl-input" id="tr-why" name="title" maxlength="120" required></div>
61
76
  <div class="hrl-field"><label for="tr-client">Client (optional)</label><input class="hrl-input" id="tr-client" name="client" maxlength="120"></div>
62
77
  <%= render "hr_lite/attendance/crm_picker", id: "tr-crm" %>
63
- <div class="hrl-field"><label for="tr-until">Back by (optional)</label><input class="hrl-input" type="time" id="tr-until" name="until"></div>
78
+ <%= render "hr_lite/attendance/duration_chips", mode: "travelling", id: "dur-travelling", optional: true %>
64
79
  <button type="submit" class="hrl-btn hrl-btn--primary">Start travelling</button>
65
80
  <% end %>
66
81
  </details>
@@ -72,7 +87,7 @@
72
87
  <div class="hrl-field"><label for="om-client">Client or prospect<%= " (or link a lead above)" if HrLite.config.crm_search %></label><input class="hrl-input" id="om-client" name="client" maxlength="120"<%= " required" unless HrLite.config.crm_search %>></div>
73
88
  <div class="hrl-field"><label for="om-why">Purpose</label><input class="hrl-input" id="om-why" name="title" maxlength="120" required></div>
74
89
  <div class="hrl-field"><label for="om-where">Where (optional)</label><input class="hrl-input" id="om-where" name="destination" maxlength="120"></div>
75
- <div class="hrl-field"><label for="om-until">Ends at (optional)</label><input class="hrl-input" type="time" id="om-until" name="until"></div>
90
+ <%= render "hr_lite/attendance/duration_chips", mode: "outside_meeting", id: "dur-om", optional: true %>
76
91
  <button type="submit" class="hrl-btn hrl-btn--primary">Start client meeting</button>
77
92
  <% end %>
78
93
  </details>
@@ -82,7 +97,7 @@
82
97
  <input type="hidden" name="mode" value="out_of_office">
83
98
  <div class="hrl-field"><label for="oo-why">Reason</label>
84
99
  <%= select_tag :category, options_for_select(HrLite::WorkModeSwitch::OUT_OF_OFFICE_REASONS.invert), id: "oo-why" %></div>
85
- <div class="hrl-field"><label for="oo-until">Back by</label><input class="hrl-input" type="time" id="oo-until" name="until" required></div>
100
+ <%= render "hr_lite/attendance/duration_chips", mode: "out_of_office", id: "dur-oo" %>
86
101
  <p class="hrl-small hrl-muted">Away for a full day? <a href="<%= hr_lite.new_leave_request_path %>">Apply for leave</a> instead.</p>
87
102
  <button type="submit" class="hrl-btn hrl-btn--primary">Set out of office</button>
88
103
  <% end %>
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.35.1"
2
+ VERSION = "0.36.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.35.1
4
+ version: 0.36.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kshitiz sinha
@@ -350,6 +350,7 @@ files:
350
350
  - app/views/hr_lite/attendance/_calendar_connection.html.erb
351
351
  - app/views/hr_lite/attendance/_crm_picker.html.erb
352
352
  - app/views/hr_lite/attendance/_custom_status.html.erb
353
+ - app/views/hr_lite/attendance/_duration_chips.html.erb
353
354
  - app/views/hr_lite/attendance/_month_grid.html.erb
354
355
  - app/views/hr_lite/attendance/_penalties.html.erb
355
356
  - app/views/hr_lite/attendance/_punch_card.html.erb