hr_lite 0.20.1 → 0.21.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: 0d52aa888b9029219bfde72b9d7eabcae22691d9bb5ec73ec6a924191cdf8ac2
4
- data.tar.gz: b1e9c215421a30a9c521059c90349db86276f0e74f3ac4c7eb4a34a05ca2cd2b
3
+ metadata.gz: 1d2f9a101bb4b88ff5c5387f7da5141c2352388216b60b74889a508826f0b1ed
4
+ data.tar.gz: 67e8da86ef1d895d5ff40a35f1d27c3a131787b154ba3c5ed68af8b0fdbf7649
5
5
  SHA512:
6
- metadata.gz: 349fcb87fb86b6ee48633227123a83e712bdd5486cda707de5f3c70d8f2b4df941747610fbf5a817336ece750f4c3d28727559faff4997c840a1fd598be83499
7
- data.tar.gz: a1c509e0a1189eeac70b67ab848ab04b3507ce85ce456d17396dca9419911187fd26021bbb997d779f07d6d51835e58c105dd4b52f0a7d3ee788899a3781ca9d
6
+ metadata.gz: f3365b0fec9bacdbddb3118a634fb781552b032f59c20b0375de141052b018f7960bb76c7354b8d48edb14236134bdfbc3a2040932387fe8ef406305cf99c74d
7
+ data.tar.gz: 5aac23f5baf4c64b1266580e8785360ce10d2033ee68d2d01d3965329a8c0c18e7daedb58e96ae478444099e4d5a01845fb5a7afc0dcf014eff594ab9996960a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.21.0] - 2026-10-06
11
+
12
+ Three migrations: `hr_lite_employee_profiles.probation_until` (date),
13
+ `hr_lite_leave_requests.half_day_part` and
14
+ `hr_lite_attendance_records.half_day_part` (string).
15
+
16
+ ### Added
17
+
18
+ - **Probation.** `config.probation_months` (e.g. 6) gives every new hire a
19
+ `probation_until` date of joining + N months. HR edits it per employee on
20
+ the profile form: blank removes the tag, a date adds or extends it.
21
+ While on probation an employee can take at most 1 day of leave (any type,
22
+ half days count 0.5) per calendar month; the request is refused at apply.
23
+ A "Probation" badge shows on the employee list, the admin profile and the
24
+ employee's own profile, and drops off by itself after the date.
25
+ Existing employees are not tagged — the host backfills or HR sets them.
26
+ - **Full day / First half / Second half.** The leave forms (employee and
27
+ HR-recorded) and HR's attendance day form use one "Day" picker instead
28
+ of a half-day checkbox / status select. A half still counts 0.5; the
29
+ label reads "05 Jul (first half)", and the team board shows which half.
30
+
31
+ ## [0.20.2] - 2026-10-05
32
+
33
+ No migration.
34
+
35
+ ### Changed
36
+
37
+ - **Leave team notice goes out on apply, by email too.** The whole team
38
+ (minus the applicant) gets the bell and an email the moment leave is
39
+ applied, not after approval. The reason is still left out. Hosts can
40
+ mute either channel with the `leave.team_notice` matrix row.
41
+ - **A blocked location holds the punch.** Browsers ask for location once;
42
+ after a "Don't allow" every later punch was filed without GPS and
43
+ flagged. The punch now waits and tells the employee to allow location,
44
+ then tap again. Timeout and no-GPS devices still punch (and flag).
45
+ - Team attendance board: "Fix / month" is now "Edit / half day", the
46
+ screen where HR changes a marked day to a half day.
47
+
10
48
  ## [0.20.1] - 2026-10-03
11
49
 
12
50
  Security fixes. No migration.
@@ -6,7 +6,7 @@
6
6
  // <button type="submit">…</button>
7
7
  // <span data-hrl-geo-status hidden></span>
8
8
  // </form>
9
- // The punch NEVER blocks on GPS: denied/timeout/unavailable submit anyway
9
+ // A denied permission holds the punch; timeout/unavailable submit anyway
10
10
  // with geo_status set; the server flags them.
11
11
  (function () {
12
12
  "use strict";
@@ -45,7 +45,15 @@
45
45
  send("ok");
46
46
  },
47
47
  function (err) {
48
- send(err && err.code === 1 ? "denied" : "timeout");
48
+ // Browsers ask only once; a past "Don't allow" denies silently forever.
49
+ // Hold the punch so it is never filed (and flagged) without GPS.
50
+ if (err && err.code === 1) {
51
+ locating = false;
52
+ if (button) { button.disabled = false; button.removeAttribute("aria-busy"); }
53
+ if (label) label.textContent = "Location is blocked. Allow location for this site in your browser settings, then tap again.";
54
+ return;
55
+ }
56
+ send("timeout");
49
57
  },
50
58
  { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 }
51
59
  );
@@ -37,7 +37,7 @@ module HrLite
37
37
  alert: "A regularization note is required."
38
38
  end
39
39
 
40
- attrs = params.require(:attendance_record).permit(:check_in_at, :check_out_at, :status)
40
+ attrs = params.require(:attendance_record).permit(:check_in_at, :check_out_at, :status, :half_day_part)
41
41
  if attrs[:check_in_at].blank? && attrs[:check_out_at].blank?
42
42
  # Nothing to remove used to still write a `destroy` audit row with
43
43
  # subject_id 0 and email the employee that a punch was removed.
@@ -61,6 +61,8 @@ module HrLite
61
61
  end
62
62
 
63
63
  record.assign_attributes(attrs)
64
+ record.half_day_part = attrs[:half_day_part].presence if attrs.key?(:half_day_part)
65
+ record.status = record.half_day_part ? "half_day" : "present" if attrs.key?(:half_day_part)
64
66
  record.status = "present" if record.status.blank?
65
67
  record.regularized_by_id = hr_current_user.id
66
68
  record.regularized_at = Time.current
@@ -113,7 +113,7 @@ module HrLite
113
113
  def profile_params
114
114
  params.require(:employee_profile).permit(
115
115
  :user_id, :manager_id, :designation, :date_of_birth, :date_of_joining,
116
- :date_of_exit, :department, :work_location, :pan_number, :pf_uan, :esi_number,
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
119
  )
@@ -25,7 +25,7 @@ module HrLite
25
25
 
26
26
  def create
27
27
  profile = recordable.find_by(employee_code: params[:employee_code].to_s.strip)
28
- @request = LeaveRequest.new(params.require(:leave_request).permit(:leave_type_id, :start_date, :end_date, :half_day, :reason)
28
+ @request = LeaveRequest.new(params.require(:leave_request).permit(:leave_type_id, :start_date, :end_date, :half_day, :half_day_part, :reason)
29
29
  .merge(user_id: profile&.user_id, created_by_id: hr_current_user.id))
30
30
  recorded = profile && LeaveRequest.transaction do
31
31
  (@request.save && @request.approve!(actor: hr_current_user, note: "Recorded by HR")) || raise(ActiveRecord::Rollback)
@@ -52,7 +52,7 @@ module HrLite
52
52
  end
53
53
 
54
54
  def request_params
55
- params.require(:leave_request).permit(:leave_type_id, :start_date, :end_date, :half_day, :reason)
55
+ params.require(:leave_request).permit(:leave_type_id, :start_date, :end_date, :half_day, :half_day_part, :reason)
56
56
  end
57
57
  end
58
58
  end
@@ -8,6 +8,7 @@ module HrLite
8
8
 
9
9
  validates :date, presence: true, uniqueness: { scope: :user_id }
10
10
  validates :status, inclusion: { in: STATUSES }
11
+ validates :half_day_part, inclusion: { in: %w[first second] }, allow_blank: true
11
12
  validate :check_out_after_check_in
12
13
 
13
14
  scope :for_date, ->(date) { where(date: date) }
@@ -34,6 +34,10 @@ module HrLite
34
34
  attr_accessor :new_user_name, :new_user_email, :new_user_password
35
35
 
36
36
  before_validation :assign_employee_code, on: :create
37
+ before_validation(on: :create) do
38
+ months = HrLite.config.probation_months.to_i
39
+ self.probation_until ||= date_of_joining.advance(months: months) - 1 if months.positive? && date_of_joining
40
+ end
37
41
  # Without a role a new hire cannot see policies or raise an HR request.
38
42
  after_create { (role = Role.find_by(name: Role::EMPLOYEE)) && RoleAssignment.find_or_create_by!(user_id:, role:) }
39
43
 
@@ -57,6 +61,12 @@ module HrLite
57
61
  .where("date_of_exit IS NULL OR date_of_exit >= ?", month.beginning_of_month)
58
62
  }
59
63
 
64
+ # Admin owns probation_until: blank = no probation tag, a date = tagged
65
+ # through that day. New hires default to config.probation_months.
66
+ def on_probation?(on = Date.current)
67
+ probation_until.present? && on <= probation_until
68
+ end
69
+
60
70
  def active_on?(date)
61
71
  date_of_joining <= date && (date_of_exit.nil? || date_of_exit >= date)
62
72
  end
@@ -12,6 +12,7 @@ module HrLite
12
12
 
13
13
  validates :start_date, :end_date, presence: true
14
14
  validates :status, inclusion: { in: STATUSES }
15
+ validates :half_day_part, inclusion: { in: %w[first second] }, allow_blank: true
15
16
 
16
17
  with_options on: :create do
17
18
  validate :end_after_start
@@ -21,10 +22,13 @@ module HrLite
21
22
  validate :no_overlap_with_own_requests
22
23
  validate :no_punch_conflict
23
24
  validate :sufficient_balance
25
+ validate :within_probation_cap
24
26
  end
25
27
 
28
+ before_validation { self.half_day = true if half_day_part.present? }
26
29
  before_validation :cache_days_count, on: :create
27
30
  after_create :notify_requested, unless: :recorded_by_hr?
31
+ after_create :notify_team
28
32
 
29
33
  scope :pending, -> { where(status: "pending") }
30
34
  scope :approved, -> { where(status: "approved") }
@@ -121,7 +125,7 @@ module HrLite
121
125
 
122
126
  def date_range_label
123
127
  if start_date == end_date
124
- "#{start_date.strftime('%d %b')}#{half_day ? ' (half day)' : ''}"
128
+ "#{start_date.strftime('%d %b')}#{half_day ? " (#{half_day_part.present? ? "#{half_day_part} half" : 'half day'})" : ''}"
125
129
  else
126
130
  "#{start_date.strftime('%d %b')} – #{end_date.strftime('%d %b')}"
127
131
  end
@@ -171,7 +175,6 @@ module HrLite
171
175
  return false if insufficient
172
176
 
173
177
  notify_decision("Leave approved")
174
- notify_team
175
178
  true
176
179
  end
177
180
 
@@ -247,8 +250,8 @@ module HrLite
247
250
  )
248
251
  end
249
252
 
250
- # Everyone should know a colleague will be away — bell + email to the
251
- # whole team (matrix row "leave.team_notice"; hosts can mute either
253
+ # Everyone should know a colleague will be away, the moment they apply —
254
+ # bell + email to the whole team (matrix row "leave.team_notice"; hosts can mute either
252
255
  # channel). Deliberately excludes the reason: dates are team-relevant,
253
256
  # the why is not.
254
257
  def notify_team
@@ -329,6 +332,16 @@ module HrLite
329
332
  errors.add(:base, "Attendance is marked in this period") if punched.exists?
330
333
  end
331
334
 
335
+ # Probation: at most one day of leave (any type) in a calendar month.
336
+ def within_probation_cap
337
+ return unless start_date && days_count
338
+ return unless EmployeeProfile.find_by(user_id: user_id)&.on_probation?(start_date)
339
+
340
+ taken = self.class.where(user_id: user_id, status: %w[pending approved])
341
+ .where(start_date: start_date.all_month).sum(:days_count)
342
+ errors.add(:base, "During probation only 1 day of leave a month is allowed") if taken + days_count > 1
343
+ end
344
+
332
345
  # Only comp-off is refused past its balance: it is earned credit, never
333
346
  # borrowed. Any other type's excess becomes loss of pay at approval.
334
347
  def sufficient_balance
@@ -36,14 +36,14 @@
36
36
  <% if record&.flagged? %>
37
37
  <span class="hrl-badge hrl-badge--bad" title="<%= record.flag_note %>">Flagged</span>
38
38
  <% elsif record&.check_in_at %>
39
- <span class="hrl-badge hrl-badge--ok"><%= record.status.humanize %></span>
39
+ <span class="hrl-badge hrl-badge--ok"><%= record.status.humanize %><%= " (#{record.half_day_part} half)" if record.half_day_part %></span>
40
40
  <% else %>
41
41
  <span class="hrl-badge hrl-badge--muted">No punch</span>
42
42
  <% end %>
43
43
  <% if record&.regularized? %><span class="hrl-badge">Fixed</span><% end %>
44
44
  </td>
45
45
  <td data-label="">
46
- <a class="hrl-small" href="<%= hr_lite.admin_attendance_path(employee.id, date: @date, month: @date.strftime("%Y-%m")) %>">Fix / month</a>
46
+ <a class="hrl-small" href="<%= hr_lite.admin_attendance_path(employee.id, date: @date, month: @date.strftime("%Y-%m")) %>">Edit / half day</a>
47
47
  </td>
48
48
  </tr>
49
49
  <% end %>
@@ -38,9 +38,8 @@
38
38
  <p class="hrl-hint">Clear both times to remove the punch entirely.</p>
39
39
  </div>
40
40
  <div class="hrl-field">
41
- <%= f.label :status %>
42
- <%= f.select :status, HrLite::AttendanceRecord::STATUSES.map { |s| [ s.humanize, s ] },
43
- selected: @edit_record.status %>
41
+ <%= f.label :half_day_part, "Day" %>
42
+ <%= f.select :half_day_part, HrLite::DAY_PARTS, selected: @edit_record.half_day_part.to_s %>
44
43
  </div>
45
44
  <div class="hrl-field">
46
45
  <%= f.label :regularization_note, "Why is this being fixed? (required)" %>
@@ -62,6 +62,10 @@
62
62
  <%= f.select :blood_group, HrLite::EmployeeProfile::BLOOD_GROUPS, include_blank: "—" %>
63
63
  </div>
64
64
  <div class="hrl-field"><%= f.label :emergency_contact, "Emergency contact (printed on the ID card)" %><%= f.telephone_field :emergency_contact %></div>
65
+ <div class="hrl-field">
66
+ <%= f.label :probation_until, "On probation until (blank = not on probation)" %>
67
+ <%= f.date_field :probation_until %>
68
+ </div>
65
69
  <div class="hrl-field">
66
70
  <%= f.label :date_of_exit, "Date of exit (leave blank while employed)" %>
67
71
  <%= f.date_field :date_of_exit %>
@@ -16,7 +16,8 @@
16
16
  <tr>
17
17
  <td data-label="Code"><%= profile.employee_code %></td>
18
18
  <td data-label="Name"><%= hr_display_name(profile.user) %>
19
- <% if profile.date_of_exit %><span class="hrl-badge hrl-badge--muted">Exited</span><% end %></td>
19
+ <% if profile.date_of_exit %><span class="hrl-badge hrl-badge--muted">Exited</span><% end %>
20
+ <% if profile.on_probation? %><span class="hrl-badge hrl-badge--warn">Probation</span><% end %></td>
20
21
  <td data-label="Designation"><%= profile.designation %></td>
21
22
  <td data-label="Joined"><%= profile.date_of_joining.strftime("%d %b %Y") %></td>
22
23
  <td data-label=""><a class="hrl-btn" href="<%= hr_lite.admin_employee_path(profile) %>">Open</a></td>
@@ -71,7 +71,8 @@
71
71
  <dt>Designation</dt><dd><%= @profile.designation.presence || "—" %></dd>
72
72
  <dt>Reports to</dt><dd><%= @profile.manager ? hr_display_name(@profile.manager) : "—" %></dd>
73
73
  <dt>Department</dt><dd><%= @profile.department.presence || "—" %></dd>
74
- <dt>Joined</dt><dd><%= @profile.date_of_joining.strftime("%d %b %Y") %></dd>
74
+ <dt>Joined</dt><dd><%= @profile.date_of_joining.strftime("%d %b %Y") %>
75
+ <% if @profile.on_probation? %><span class="hrl-badge hrl-badge--warn">Probation</span><% end %></dd>
75
76
  <% if @profile.date_of_exit %><dt>Exited</dt><dd><%= @profile.date_of_exit.strftime("%d %b %Y") %></dd><% end %>
76
77
  <dt>Tax regime</dt><dd><%= @profile.tax_regime.humanize %></dd>
77
78
  <dt>PAN</dt><dd><%= @profile.masked_pan || "—" %></dd>
@@ -29,9 +29,9 @@
29
29
  </div>
30
30
  <div class="hrl-field"><%= f.label :start_date, "From" %><%= f.date_field :start_date %></div>
31
31
  <div class="hrl-field"><%= f.label :end_date, "To" %><%= f.date_field :end_date %></div>
32
- <div class="hrl-field hrl-field--check">
33
- <%= f.check_box :half_day %>
34
- <%= f.label :half_day, "Half day (single-day only)" %>
32
+ <div class="hrl-field">
33
+ <%= f.label :half_day_part, "Day" %>
34
+ <%= f.select :half_day_part, HrLite::DAY_PARTS %>
35
35
  </div>
36
36
  <div class="hrl-field"><%= f.label :reason, "Note (optional)" %><%= f.text_area :reason %></div>
37
37
  <div class="hrl-form-actions">
@@ -12,7 +12,8 @@
12
12
  <dd><%= (boss = @profile.active_manager) ? "#{hr_display_name(boss)} (your L1)" : "—" %>
13
13
  · <a href="<%= hr_lite.org_path %>">Org chart</a></dd>
14
14
  <dt>Department</dt><dd><%= @profile.department.presence || "—" %></dd>
15
- <dt>Joined</dt><dd><%= @profile.date_of_joining.strftime("%d %B %Y") %></dd>
15
+ <dt>Joined</dt><dd><%= @profile.date_of_joining.strftime("%d %B %Y") %>
16
+ <% if @profile.on_probation? %><span class="hrl-badge hrl-badge--warn">Probation</span><% end %></dd>
16
17
  <dt>Tax regime</dt><dd><%= @profile.tax_regime.humanize %></dd>
17
18
  <dt>PAN</dt><dd><%= @profile.masked_pan || "—" %></dd>
18
19
  <dt>PF UAN</dt><dd><%= @profile.masked_uan || "—" %></dd>
@@ -21,9 +21,9 @@
21
21
  <%= f.label :end_date, "To" %>
22
22
  <%= f.date_field :end_date %>
23
23
  </div>
24
- <div class="hrl-field hrl-field--check">
25
- <%= f.check_box :half_day %>
26
- <%= f.label :half_day, "Half day (single-day requests only)" %>
24
+ <div class="hrl-field">
25
+ <%= f.label :half_day_part, "Day" %>
26
+ <%= f.select :half_day_part, HrLite::DAY_PARTS %>
27
27
  </div>
28
28
  <div class="hrl-field">
29
29
  <%= f.label :reason, "Reason (optional)" %>
@@ -0,0 +1,5 @@
1
+ class AddProbationUntilToHrLiteEmployeeProfiles < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :hr_lite_employee_profiles, :probation_until, :date
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddHalfDayPartToHrLiteLeaveRequests < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :hr_lite_leave_requests, :half_day_part, :string
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddHalfDayPartToHrLiteAttendanceRecords < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :hr_lite_attendance_records, :half_day_part, :string
4
+ end
5
+ end
@@ -12,7 +12,9 @@ module HrLite
12
12
  :onboard_user, :offboard_user, :invite_url_for,
13
13
  # ->(user) { { url:, unread: } } — shows a notifications link in
14
14
  # the HR shell; nil (default) shows nothing.
15
- :notifications
15
+ :notifications,
16
+ # e.g. 6 — default probation for new hires (admin edits per employee)
17
+ :probation_months
16
18
 
17
19
  attr_reader :leave_year_start_month, :slip_release_day, :self_regularization
18
20
 
@@ -51,7 +51,7 @@ module HrLite
51
51
  "resignation.withdrawn" => { bell: true, email: false, leadership_email: true, leadership_bell: false },
52
52
  "employee.onboarded" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
53
53
  "payroll.draft_ready" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
54
- "leave.team_notice" => { bell: true, email: false, leadership_email: false, leadership_bell: false },
54
+ "leave.team_notice" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
55
55
  "comp_off.requested" => { bell: true, email: false, leadership_email: true, leadership_bell: true },
56
56
  "comp_off.approved" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
57
57
  "comp_off.rejected" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.20.1"
2
+ VERSION = "0.21.0"
3
3
  end
data/lib/hr_lite.rb CHANGED
@@ -17,6 +17,8 @@ require "hr_lite/amount_in_words"
17
17
  require "hr_lite/statutory_rate_card"
18
18
 
19
19
  module HrLite
20
+ # Full day / first half / second half — one picker for leave and attendance.
21
+ DAY_PARTS = [ [ "Full day", "" ], [ "First half", "first" ], [ "Second half", "second" ] ].freeze
20
22
  class << self
21
23
  def config
22
24
  @config ||= Configuration.new
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.20.1
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kshitiz sinha
@@ -376,6 +376,9 @@ files:
376
376
  - db/migrate/20260927100145_add_id_card_fields_to_hr_lite_employee_profiles.rb
377
377
  - db/migrate/20260927101031_add_created_by_id_to_hr_lite_leave_requests.rb
378
378
  - db/migrate/20260929081109_add_paid_days_to_hr_lite_leave_requests.rb
379
+ - db/migrate/20261006090000_add_probation_until_to_hr_lite_employee_profiles.rb
380
+ - db/migrate/20261006120000_add_half_day_part_to_hr_lite_leave_requests.rb
381
+ - db/migrate/20261006120100_add_half_day_part_to_hr_lite_attendance_records.rb
379
382
  - lib/generators/hr_lite/install/install_generator.rb
380
383
  - lib/generators/hr_lite/install/templates/AFTER_INSTALL
381
384
  - lib/generators/hr_lite/install/templates/initializer.rb