hr_lite 0.56.0 → 0.57.1

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: 5deeac7b0eead75adc631a3fe868844a5db7712aaa8297089d6a7ab305c1c610
4
- data.tar.gz: fd6b50aa2589eaba50c53703f3a8d4c22a96c61d469989045e1dc7113005a546
3
+ metadata.gz: 288b37a483def4fc57b059096e2ee8af708d1245dd2587119855635ea73d9c11
4
+ data.tar.gz: 65fe286df5fb17b8b369ab499b2980e3456b69f92b4f823f89c75e1053af7f35
5
5
  SHA512:
6
- metadata.gz: 14b6853b122a691166a5f2b884a52451d3893bff7b82d33df1ab5e69bfbf71dc00644fe07221588f914a2e7082b95e1164a64f7545d830ccb26c88792c82a701
7
- data.tar.gz: 5dbf80c20826f0f3baaff056627d2bb7478914dfce0eb3e140976564e8d60fc4e4a98684fdf37b2cc3c77f5fbc5de5314b6048470fec9c33af7baaeb7053f06e
6
+ metadata.gz: 955e1a2242f0c9585b219c5896b9069253624c9797aaa1028ecc7c068f5e5cde734af64ddfcf4982696a4b066e66bb322ed67e37a46d5b1587d5ff04e4d14ddc
7
+ data.tar.gz: 57ef0949c6c9f8e4569b8b0157b212ba52ad19f226541f5977d32f8773e1555e444b91eda89ccdc350df84c7bf9db51c279bfd3830aff994c487286c102a6693
data/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.57.1] - 2026-10-19
11
+
12
+ Forms in the host's iOS and Android staff apps (D24). No migrations.
13
+
14
+ - **A save shows its confirmation again.** Native pages reload on every Turbo visit, and the page Turbo fetched and threw away used to spend the flash. A GET fetched by Turbo in the app now keeps it for the reload.
15
+ - **A refused form keeps its errors and what was typed.** A 422 used to be discarded and the form reloaded blank. It is now drawn in place (with `turbo-cache-control: no-cache`), and the next visit reloads as before.
16
+ - Browsers are unchanged: they never load Turbo.
17
+
18
+ ## [0.57.0] - 2026-10-19
19
+
20
+ No migration.
21
+
22
+ - **Why a day is Absent.** A refused check-in or check-out is now saved with its reason, and the person's attendance summary lists it under that day, for example "Check-in tried 10:09 — refused: You are not within the office location…". When the phone holds a tap because location is blocked or not found, the browser tells the server, and the day shows "Location not picked up on the phone (denied)". A past Absent day with no tries says "No check-in tried". Days before the first refusal was ever saved show neither line, because there is no record to go on. The rows are `attendance.punch_refused` entries in the audit trail.
23
+
10
24
  ## [0.56.0] - 2026-10-17
11
25
 
12
26
  Owner decisions on the HRMS audit (#133–#138). **Two migrations**: the host runs `bin/rails db:migrate` and refreshes `db/schema.rb`.
@@ -88,10 +88,17 @@
88
88
  form.submit();
89
89
  }
90
90
 
91
- function hold(headline) {
91
+ function hold(headline, status) {
92
92
  locating = false;
93
93
  if (button) { button.disabled = false; button.removeAttribute("aria-busy"); }
94
94
  showHelp(label, headline);
95
+ // Tell the server about the tap, so HR sees why the day has no check-in.
96
+ if (/\/check_(in|out)$/.test(form.action) && navigator.sendBeacon) {
97
+ var data = new FormData(form);
98
+ data.set("held", "1");
99
+ data.set("geo_status", status);
100
+ navigator.sendBeacon(form.action, data);
101
+ }
95
102
  }
96
103
 
97
104
  form.addEventListener("submit", function (event) {
@@ -102,7 +109,7 @@
102
109
  if (label) { label.hidden = false; label.classList.replace("hrl-errors", "hrl-muted"); label.textContent = "Getting location…"; }
103
110
 
104
111
  if (!navigator.geolocation) {
105
- if (required) hold("This browser cannot share your location. Open this page in Chrome or Safari.");
112
+ if (required) hold("This browser cannot share your location. Open this page in Chrome or Safari.", "unavailable");
106
113
  else send("unavailable");
107
114
  return;
108
115
  }
@@ -117,8 +124,8 @@
117
124
  function (err) {
118
125
  // Browsers ask only once; a past "Don't allow" denies silently forever.
119
126
  // Hold the punch so it is never filed (and flagged) without GPS.
120
- if (err && err.code === 1) { hold("Location is blocked. To allow it:"); return; }
121
- if (required) { hold("Couldn't get your location. Check that location is on:"); return; }
127
+ if (err && err.code === 1) { hold("Location is blocked. To allow it:", "denied"); return; }
128
+ if (required) { hold("Couldn't get your location. Check that location is on:", err && err.code === 3 ? "timeout" : "unavailable"); return; }
122
129
  send("timeout");
123
130
  },
124
131
  { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 }
@@ -141,6 +141,10 @@ module HrLite
141
141
  range = @period.from..@period.to
142
142
  @days = DayStatus.new(user: @employee, range: range)
143
143
  @punches = AttendancePunch.where(user_id: @employee.id, date: range).order(:at, :id).group_by(&:date)
144
+ refused = AuditLog.where(action: "attendance.punch_refused")
145
+ @refused = refused.where(subject_type: HrLite.user_klass.name, subject_id: @employee.id, created_at: range.first.beginning_of_day..range.last.end_of_day)
146
+ .order(:created_at).group_by { |log| log.created_at.to_date }
147
+ @tracked_from = refused.minimum(:created_at)&.to_date # before this, a day with no tries proves nothing
144
148
  @penalties = AttendancePenalty.active.where(user_id: @employee.id, date: range).group(:date).sum(:minutes)
145
149
  @balances = LeaveType.active.where(paid: true).map { |type| LeaveBalance.for(@employee, type, LeaveYear.current_key) }
146
150
  @requests = summary_requests(@employee, range)
@@ -9,6 +9,9 @@ module HrLite
9
9
  before_action :hr_authenticate!
10
10
  before_action :hr_set_current_actor
11
11
  around_action :hr_use_time_zone
12
+ # Native pages reload every Turbo visit (see the layout): the page Turbo
13
+ # fetched and threw away must not spend the flash the reload shows.
14
+ after_action -> { flash.keep if request.get? && request.headers["X-Turbo-Request-Id"] && HrLite.config.native_app.call(request) }
12
15
 
13
16
  helper_method :hr_current_user, :hr_admin?, :hr_leadership?, :hr_superadmin?, :hr_operations?, :hr_display_name,
14
17
  :hr_can?, :hr_reaches?, :hr_access
@@ -51,6 +51,13 @@ module HrLite
51
51
  private
52
52
 
53
53
  def punch(kind)
54
+ # The browser held the tap (location blocked or not found): log it, file nothing.
55
+ if params[:held]
56
+ geo = AttendancePuncher::GEO_STATUSES.include?(params[:geo_status]) ? params[:geo_status] : "unavailable"
57
+ log_refused(kind, "Location not picked up on the phone (#{geo})")
58
+ return head(:no_content)
59
+ end
60
+
54
61
  field = params.fetch(:field, {}).permit(:mode, :purpose, :place).to_h
55
62
  result = AttendancePuncher.call(user: hr_current_user, kind: kind, field: field, travel: params[:place] == "travel",
56
63
  save_home: params[:save_home] == "1" || params[:place] == "home", **punch_params.to_h.symbolize_keys)
@@ -58,11 +65,18 @@ module HrLite
58
65
  time = Time.current.strftime("%H:%M")
59
66
  redirect_to attendance_path, notice: "#{kind == :check_in ? 'Checked in' : 'Checked out'} at #{time}."
60
67
  else
68
+ log_refused(kind, result.error)
61
69
  flash[:punch_offer] = result.offer.to_s if result.offer
62
70
  redirect_to attendance_path, alert: result.error
63
71
  end
64
72
  end
65
73
 
74
+ # The person's summary lists these per day, so HR can see why a day is Absent.
75
+ def log_refused(kind, error)
76
+ AuditLog.record!(action: "attendance.punch_refused", subject: hr_current_user, actor: hr_current_user,
77
+ changes: { "kind" => kind.to_s, "error" => error })
78
+ end
79
+
66
80
  def punch_params
67
81
  params.permit(:lat, :lng, :accuracy_m, :geo_status)
68
82
  end
@@ -45,6 +45,12 @@
45
45
  <% if punches.size > 2 %><div>All punches: <%= punches.join(" · ") %></div><% end %>
46
46
  <% if (minutes = @penalties[date]) %><div><span class="hrl-badge hrl-badge--warn">+<%= minutes %> min</span> penalty</div><% end %>
47
47
  <% if rec&.flagged? %><div>Flagged: <%= rec.flag_note %></div><% end %>
48
+ <% @refused[date].to_a.each do |log| %>
49
+ <div><%= log.audited_changes["kind"] == "check_out" ? "Check-out" : "Check-in" %> tried <%= log.created_at.strftime("%H:%M") %> — refused: <%= log.audited_changes["error"] %></div>
50
+ <% end %>
51
+ <% if day.kind == :absent && !not_in_yet && @refused[date].nil? && @tracked_from && date >= @tracked_from %>
52
+ <div class="hrl-muted">No check-in tried</div>
53
+ <% end %>
48
54
  </div>
49
55
  <% if fix && date <= Date.current %>
50
56
  <a class="hrl-btn" href="<%= hr_lite.admin_attendance_path(@employee.id, date: date, month: date.strftime("%Y-%m"), anchor: "regularize") %>">Fix<span class="hrl-sr-only"> <%= date.strftime("%-d %b") %></span></a>
@@ -10,8 +10,14 @@
10
10
  <% if HrLite.config.native_app.call(request) %>
11
11
  <%# The host's iOS/Android shells drive pages through Turbo. "reload" makes
12
12
  every visit a full page load, so the plain scripts below run exactly
13
- as they do in a browser. Browsers never load Turbo here. %>
14
- <meta name="turbo-visit-control" content="reload">
13
+ as they do in a browser. Browsers never load Turbo here. A refused
14
+ form (422) is drawn in place instead: the reload would bring the form
15
+ back blank, with no error. no-cache keeps that render out of history. %>
16
+ <% if response.unprocessable? %>
17
+ <meta name="turbo-cache-control" content="no-cache">
18
+ <% else %>
19
+ <meta name="turbo-visit-control" content="reload">
20
+ <% end %>
15
21
  <%= javascript_include_tag "hr_lite/vendor/turbo.min", type: "module" %>
16
22
  <% end %>
17
23
  <%= stylesheet_link_tag "hr_lite/hr_lite", media: "all" %>
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.56.0"
2
+ VERSION = "0.57.1"
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.56.0
4
+ version: 0.57.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - kshitiz sinha