hr_lite 0.26.0 → 0.28.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: 54c475874de00a0db443998c37e4caeb3340e202584e094db29ef423c4902698
4
- data.tar.gz: 849526338af6b16a5648a5163d140f0cda40b2fa3c34ef2a63a3d2f454761132
3
+ metadata.gz: 175c2fc4265dd0055f39926d529060dedeb14386991eb9f43d1b3f0e80112dce
4
+ data.tar.gz: 733cc4423053d7ca4b8d27acd03165dd073481f767131731179432392a3be335
5
5
  SHA512:
6
- metadata.gz: 38bf1cd87f1e283d7e8a54e60a18a19020fae7ff52feca24941f94dc40673c29d3a08a99d8c82699353f9958a99ffaf39ab8ae0a6c2e5e237ee1d96a567ae0c7
7
- data.tar.gz: 390843351f21b51a9f2f17f1159214035349ecf8a689722d8dd429b803775715b322d6914fc251bb1808ad03c09c569f55beb0c85b634de8fefa41069b6317f1
6
+ metadata.gz: d26ff16fb6dc9bc592374a0920d659556b7ad8d466fccfa6a24084bfada3bd95fd4b756e2de2ae01138dab005d7e612fd58883b80e1a7ef333f53bd75748f191
7
+ data.tar.gz: 96b6f9c358ecba6d459da48a97cbdc866f5dbabb1a7ab1dff4345961cdf4e6780c0ae4f7dd56635a66aa41a5078b830423cb1740eada0acda5c4350b4d84590d
data/CHANGELOG.md CHANGED
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.28.0] - 2026-10-10
11
+
12
+ - HR and leadership can cancel any approved or pending leave at any time, past dates included. A reason is required; it is kept as the decision note and in the audit log. Employees still cancel their own leave only before it starts. A cancelled past day with no punch shows as absent.
13
+ - Leave always accrues from the joining date. Somebody with no joining date (no HR profile) now accrues nothing instead of a full year, and the team balances grid flags them with "No joining date".
14
+
15
+ ## [0.27.0] - 2026-10-10
16
+
17
+ - HR and admins can fix leave they recorded (type, dates, half day) at any time, past dates included, without cancelling it. A reason is required; it is kept as the decision note and in the audit log (`leave.corrected`), and the balance and loss-of-pay split are worked out again.
18
+
10
19
  ## [0.26.0] - 2026-10-10
11
20
 
12
21
  Two migrations: `hr_lite_ca_reviews`, and a data migration creating the CA role and giving Super Admin the new `payroll.verify` permission. Schedule `CaReviewReminderJob` weekly and `CaReviewOverdueJob` on 31 March.
@@ -5,6 +5,7 @@ module HrLite
5
5
  @year = sanitized_year
6
6
  @types = LeaveType.active.where(paid: true).where.not(annual_quota: nil)
7
7
  @employees = HrLite.employees.select { |employee| hr_reaches?("leave.view", employee) }
8
+ @no_joining_date = @employees.map(&:id) - EmployeeProfile.where(user_id: @employees.map(&:id)).where.not(date_of_joining: nil).pluck(:user_id)
8
9
  end
9
10
 
10
11
  # Manual credit/debit — also the comp-off credit mechanism.
@@ -37,6 +37,23 @@ module HrLite
37
37
  render :new, status: :unprocessable_entity
38
38
  end
39
39
 
40
+ # Leave HR recorded is fixed in place, any time, with a reason.
41
+ def edit
42
+ @request = find_recorded
43
+ end
44
+
45
+ def update
46
+ @request = find_recorded
47
+ reason = params[:correction_reason].to_s.strip
48
+ @request.errors.add(:base, "A reason is required to fix this leave") if reason.blank?
49
+ attrs = params.require(:leave_request).permit(:leave_type_id, :start_date, :end_date, :half_day_part)
50
+ if reason.present? && @request.correct!(actor: hr_current_user, attrs: attrs, reason: reason)
51
+ return redirect_to(admin_leave_request_path(@request), notice: "Leave fixed.")
52
+ end
53
+
54
+ render :edit, status: :unprocessable_entity
55
+ end
56
+
40
57
  def approve
41
58
  request = find_decidable
42
59
  if request.approve!(actor: hr_current_user, note: params[:decision_note].presence)
@@ -56,12 +73,14 @@ module HrLite
56
73
  # not be released at all, and the quota stayed spent.
57
74
  def cancel
58
75
  request = find_decidable
59
- if request.cancellable_by?(hr_current_user)
60
- request.cancel!(actor: hr_current_user)
61
- redirect_to admin_leave_requests_path, notice: "Leave cancelled — the balance is released."
76
+ note = params[:decision_note].to_s.strip
77
+ if !request.cancellable_by?(hr_current_user)
78
+ redirect_to admin_leave_request_path(request), alert: "This leave can no longer be cancelled."
79
+ elsif note.blank?
80
+ redirect_to admin_leave_request_path(request), alert: "A reason is required to cancel."
62
81
  else
63
- redirect_to admin_leave_request_path(request),
64
- alert: "Only pending leave, or approved leave that has not started, can be cancelled."
82
+ request.cancel!(actor: hr_current_user, note: note)
83
+ redirect_to admin_leave_requests_path, notice: "Leave cancelled — the balance is released."
65
84
  end
66
85
  end
67
86
 
@@ -91,6 +110,8 @@ module HrLite
91
110
 
92
111
  def find_decidable = decidable.find(params[:id])
93
112
 
113
+ def find_recorded = decidable.approved.where.not(created_by_id: nil).where("created_by_id <> user_id").find(params[:id])
114
+
94
115
  def recordable = hr_scope(EmployeeProfile.includes(:user), "leave.approve").where.not(user_id: hr_current_user.id)
95
116
  end
96
117
  end
@@ -97,7 +97,8 @@ module HrLite
97
97
  # nil when the person joins only after this leave year ends.
98
98
  def accrual_start(range)
99
99
  doj = employment_window.first
100
- return range.first if doj.nil? || doj <= range.first
100
+ return nil if doj.nil? # no joining date, no accrual — never a full year by default
101
+ return range.first if doj <= range.first
101
102
 
102
103
  start = doj.day <= 15 ? doj.beginning_of_month : doj.next_month.beginning_of_month
103
104
  start > range.last ? nil : start
@@ -91,23 +91,25 @@ module HrLite
91
91
 
92
92
  # Owner may cancel while pending, or an approved future leave (quota
93
93
  # returns automatically because `used` is computed). Admins may cancel
94
- # any pending/approved future leave.
94
+ # anybody else's pending/approved leave at any time, past dates included.
95
95
  def cancellable_by?(actor)
96
- return false unless pending? || (approved? && start_date > Date.current)
96
+ return false unless pending? || approved?
97
+ return pending? || start_date > Date.current if user_id == actor.id
97
98
 
98
- user_id == actor.id || HrLite.admin?(actor) || HrLite.leadership?(actor)
99
+ HrLite.admin?(actor) || HrLite.leadership?(actor)
99
100
  end
100
101
 
101
- def cancel!(actor:)
102
+ def cancel!(actor:, note: nil)
102
103
  was_approved = approved?
103
104
  transaction do
104
105
  self.status = "cancelled"
105
106
  self.decided_by_id = actor.id
106
107
  self.decided_at = Time.current
108
+ self.decision_note = note if note.present?
107
109
  save!
108
110
  # Cancelling an APPROVED leave hands the days back to the balance, so
109
111
  # it is a quota change as much as a status change.
110
- audit!("cancelled", actor, was_approved ? "was approved" : nil)
112
+ audit!("cancelled", actor, [ note.presence, was_approved ? "was approved" : nil ].compact.join("; ").presence)
111
113
  # And nobody should still be asked to decide a request that has been
112
114
  # called off — it would sit in their inbox for ever.
113
115
  approval_route.cancel_all!
@@ -123,6 +125,24 @@ module HrLite
123
125
  true
124
126
  end
125
127
 
128
+ # HR fixes leave it recorded — any time, even past dates — without a
129
+ # cancel. Re-run as a fresh recording so the create checks and the
130
+ # balance / loss-of-pay split are worked out exactly as the first time.
131
+ def correct!(actor:, attrs:, reason:)
132
+ fixed = false
133
+ transaction do
134
+ assign_attributes(attrs.merge(status: "pending", paid_days: nil, half_day: false))
135
+ raise ActiveRecord::Rollback unless valid?(:create) && save(validate: false)
136
+
137
+ audit!("corrected", actor, reason)
138
+ fixed = approve_outright!(actor: actor, note: "Corrected: #{reason}") || raise(ActiveRecord::Rollback)
139
+ end
140
+ errors.add(:base, "Not enough #{leave_type.name} balance") unless fixed || errors.any?
141
+ fixed
142
+ end
143
+
144
+ def recorded_by_hr? = created_by_id.present? && created_by_id != user_id
145
+
126
146
  def date_range_label
127
147
  if start_date == end_date
128
148
  "#{start_date.strftime('%d %b')}#{half_day ? " (#{half_day_part.present? ? "#{half_day_part} half" : 'half day'})" : ''}"
@@ -230,8 +250,6 @@ module HrLite
230
250
  )
231
251
  end
232
252
 
233
- def recorded_by_hr? = created_by_id.present? && created_by_id != user_id
234
-
235
253
  # The balance covers whole half-days only, never less than nothing.
236
254
  def covered_by(available) = [ (BigDecimal(available.to_s) * 2).floor / BigDecimal(2), 0 ].max
237
255
 
@@ -14,7 +14,7 @@
14
14
  <tbody>
15
15
  <% @employees.each do |employee| %>
16
16
  <tr>
17
- <td data-label="Employee"><%= hr_display_name(employee) %></td>
17
+ <td data-label="Employee"><%= hr_display_name(employee) %><% if @no_joining_date.include?(employee.id) %><br><span class="hrl-badge hrl-badge--bad">No joining date — no leave accrues</span><% end %></td>
18
18
  <% @types.each do |type| %>
19
19
  <% balance = HrLite::LeaveBalance.for(employee, type, @year) %>
20
20
  <td data-label="<%= type.code %>" class="hrl-num"><span><strong><%= hrl_days(balance.available) %></strong> left<br><span class="hrl-small hrl-muted"><%= hrl_days(balance.used) %>/<%= hrl_days(balance.entitled) %> used</span></span></td>
@@ -0,0 +1,30 @@
1
+ <% content_for(:page_title) { "Fix leave" } %>
2
+ <a class="hrl-backlink" href="<%= hr_lite.admin_leave_request_path(@request) %>"><span aria-hidden="true">‹</span> Leave</a>
3
+ <div class="hrl-page__head">
4
+ <h1 class="hrl-page__title">Fix leave — <%= hr_display_name(@request.user) %></h1>
5
+ <p class="hrl-page__sub">Change the type, dates or day. The balance and loss of pay are worked out again, and they get an email.</p>
6
+ </div>
7
+
8
+ <section class="hrl-card">
9
+ <%= form_with model: @request, url: hr_lite.admin_leave_request_path(@request), method: :patch, local: true do |f| %>
10
+ <%= render "hr_lite/shared/form_errors", record: @request %>
11
+ <div class="hrl-field">
12
+ <%= f.label :leave_type_id, "Type" %>
13
+ <%= f.collection_select :leave_type_id, HrLite::LeaveType.active, :id, :name %>
14
+ </div>
15
+ <div class="hrl-field"><%= f.label :start_date, "From" %><%= f.date_field :start_date %></div>
16
+ <div class="hrl-field"><%= f.label :end_date, "To" %><%= f.date_field :end_date %></div>
17
+ <div class="hrl-field">
18
+ <%= f.label :half_day_part, "Day" %>
19
+ <%= f.select :half_day_part, HrLite::DAY_PARTS %>
20
+ </div>
21
+ <div class="hrl-field">
22
+ <%= label_tag :correction_reason, "Reason for the fix (required)" %>
23
+ <%= text_area_tag :correction_reason, params[:correction_reason], required: true, class: "hrl-input" %>
24
+ </div>
25
+ <div class="hrl-form-actions">
26
+ <%= f.submit "Save fix", class: "hrl-btn hrl-btn--primary" %>
27
+ <a class="hrl-btn" href="<%= hr_lite.admin_leave_request_path(@request) %>">Back</a>
28
+ </div>
29
+ <% end %>
30
+ </section>
@@ -24,15 +24,29 @@
24
24
  </dl>
25
25
  </section>
26
26
 
27
- <%# An approved leave that has not started yet can still be called off — the
27
+ <% if @request.approved? && @request.recorded_by_hr? %>
28
+ <section class="hrl-card">
29
+ <h2 class="hrl-card__title">Fix</h2>
30
+ <p class="hrl-small hrl-muted">Recorded by HR. Change it any time — no need to cancel.</p>
31
+ <div class="hrl-form-actions">
32
+ <a class="hrl-btn" href="<%= hr_lite.edit_admin_leave_request_path(@request) %>">Fix this leave</a>
33
+ </div>
34
+ </section>
35
+ <% end %>
36
+
37
+ <%# HR can call off approved leave at any time, past dates included — the
28
38
  balance is recomputed live, so cancelling gives the days straight back. %>
29
39
  <% if @request.cancellable_by?(hr_current_user) && !@request.pending? %>
30
40
  <section class="hrl-card">
31
41
  <h2 class="hrl-card__title">Cancel</h2>
32
- <p class="hrl-small hrl-muted">Releases the days back to their balance. Only possible
33
- before the leave starts.</p>
42
+ <p class="hrl-small hrl-muted">Releases the days back to their balance. A past day with
43
+ no punch will then show as absent.</p>
34
44
  <%= form_with url: hr_lite.cancel_admin_leave_request_path(@request), method: :post, local: true,
35
45
  html: { data: { turbo_confirm: "Cancel this approved leave?" } } do %>
46
+ <div class="hrl-field">
47
+ <label for="cancel_note">Reason (required)</label>
48
+ <textarea name="decision_note" id="cancel_note" class="hrl-input" required></textarea>
49
+ </div>
36
50
  <div class="hrl-form-actions">
37
51
  <button class="hrl-btn hrl-btn--danger" type="submit">Cancel this leave</button>
38
52
  </div>
data/config/routes.rb CHANGED
@@ -60,7 +60,7 @@ HrLite::Engine.routes.draw do
60
60
  resources :attendances, only: %i[index show update], param: :user_id do
61
61
  collection { get :week }
62
62
  end
63
- resources :leave_requests, only: %i[index show new create] do
63
+ resources :leave_requests, only: %i[index show new create edit update] do
64
64
  member { post :approve; post :reject; post :cancel }
65
65
  end
66
66
  resources :leave_balances, only: :index do
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.26.0"
2
+ VERSION = "0.28.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.26.0
4
+ version: 0.28.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kshitiz sinha
@@ -262,6 +262,7 @@ files:
262
262
  - app/views/hr_lite/admin/hr_requests/index.html.erb
263
263
  - app/views/hr_lite/admin/hr_requests/show.html.erb
264
264
  - app/views/hr_lite/admin/leave_balances/index.html.erb
265
+ - app/views/hr_lite/admin/leave_requests/edit.html.erb
265
266
  - app/views/hr_lite/admin/leave_requests/index.html.erb
266
267
  - app/views/hr_lite/admin/leave_requests/new.html.erb
267
268
  - app/views/hr_lite/admin/leave_requests/show.html.erb