hr_lite 0.4.0 → 0.5.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: 7d95fb8f07ae1876be368009a6d539b30938e7635d95abcbe9e1bbd404e57d0c
4
- data.tar.gz: a3f31cbd6ca7936381ce13f3baf2c4ba6532431f5066867345a1c6e4c30a69b0
3
+ metadata.gz: 00cc5c346c88ddc803a5336a1c14a07dd4609fc887213f99e9b11654f26fa109
4
+ data.tar.gz: 8306411151a38ea5b1fe0fe9c7b4bd3572e16917a1defc1e15d4e823ab7f9c1b
5
5
  SHA512:
6
- metadata.gz: 8efffc3c3f5468f3e15c7118ce0f50248e74a1e0b359a009022b9c40376c5b41c755c4cb350fbd68a6d90d734acfa23989f790c0143f2f815440983c52425488
7
- data.tar.gz: b020fd0ca4df79fba65e8efdc7a99da0c6979ec050405b7b9b1fe63091d62cd421725815f46f03da450edf82635336eb7ac8814ce35807f2369252bb18631bd8
6
+ metadata.gz: 6f9be2057c4023881d956fc8feadb18d2917bb38cd92746b7a448a60fedea0fc94d335b22d36a627a8ce888e36a7520ca6118ad77e6f5956b96fdec695f813dc
7
+ data.tar.gz: 95c8048ecd82dc4b92d9e4e5b79240bed1385e297206383280c76e010d141f666e57eb150eac9defdbe556978226d7b4a5fff21ff8132103e04e09862a36f3ad
data/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-07-20
11
+
12
+ ### Added
13
+
14
+ - **Superadmin (money) tier** — `config.superadmin_emails`: only these
15
+ people reach salary structures, payroll runs, slips administration,
16
+ appraisals and promotions, or see salary/appraisal data on the
17
+ employee page and the Payroll nav item. Ordinary leadership keeps
18
+ governing people and policy. Empty list (default) = leadership keeps
19
+ the money tier, exactly as before.
20
+ - **System-assigned employee codes**: prefix (Settings, default "EMP")
21
+ + zero-padded sequence — EMP001, EMP002, … Forms no longer accept a
22
+ code; changing the prefix starts a fresh sequence; explicitly-set
23
+ codes (imports/seeds) are never overwritten.
24
+ - "New employee" button on the Employees screen.
25
+
10
26
  ## [0.4.0] - 2026-07-19
11
27
 
12
28
  ### Added
@@ -1,6 +1,6 @@
1
1
  module HrLite
2
2
  module Admin
3
- class AppraisalsController < LeadershipController
3
+ class AppraisalsController < SuperadminController
4
4
  before_action :set_profile
5
5
 
6
6
  def new
@@ -1,7 +1,7 @@
1
1
  module HrLite
2
2
  module Admin
3
3
  # Standalone role change (promotion without an appraisal).
4
- class DesignationChangesController < LeadershipController
4
+ class DesignationChangesController < SuperadminController
5
5
  before_action :set_profile
6
6
 
7
7
  def new
@@ -93,7 +93,7 @@ module HrLite
93
93
 
94
94
  def profile_params
95
95
  params.require(:employee_profile).permit(
96
- :user_id, :manager_id, :employee_code, :designation, :date_of_birth, :date_of_joining,
96
+ :user_id, :manager_id, :designation, :date_of_birth, :date_of_joining,
97
97
  :date_of_exit, :department, :work_location, :pan_number, :pf_uan, :esi_number,
98
98
  :bank_account_number, :bank_ifsc, :bank_name, :tax_regime, :declared_annual_deductions
99
99
  )
@@ -1,6 +1,6 @@
1
1
  module HrLite
2
2
  module Admin
3
- class PayrollRunsController < LeadershipController
3
+ class PayrollRunsController < SuperadminController
4
4
  def index
5
5
  @runs = paginate(PayrollRun.recent_first)
6
6
  end
@@ -1,6 +1,6 @@
1
1
  module HrLite
2
2
  module Admin
3
- class SalarySlipsController < LeadershipController
3
+ class SalarySlipsController < SuperadminController
4
4
  def show
5
5
  @slip = SalarySlip.includes(:payroll_run).find(params[:id])
6
6
  @profile = @slip.user_profile
@@ -1,6 +1,6 @@
1
1
  module HrLite
2
2
  module Admin
3
- class SalaryStructuresController < LeadershipController
3
+ class SalaryStructuresController < SuperadminController
4
4
  before_action :set_profile
5
5
 
6
6
  def new
@@ -7,7 +7,7 @@ module HrLite
7
7
 
8
8
  def update
9
9
  @setting = Setting.instance
10
- if @setting.update(params.require(:setting).permit(:weekend_policy))
10
+ if @setting.update(params.require(:setting).permit(:weekend_policy, :employee_code_prefix))
11
11
  redirect_to edit_admin_setting_path, notice: "Settings saved."
12
12
  else
13
13
  render :edit, status: :unprocessable_entity
@@ -0,0 +1,16 @@
1
+ module HrLite
2
+ module Admin
3
+ # Money tier: salary structures, payroll, slips, appraisals and
4
+ # promotions. Only config.superadmin_emails — ordinary leadership can
5
+ # govern policy and people but never sees another person's pay.
6
+ class SuperadminController < LeadershipController
7
+ before_action :require_hr_superadmin!
8
+
9
+ private
10
+
11
+ def require_hr_superadmin!
12
+ hr_access_denied unless hr_superadmin?
13
+ end
14
+ end
15
+ end
16
+ end
@@ -10,7 +10,7 @@ module HrLite
10
10
  before_action :hr_set_current_actor
11
11
  around_action :hr_use_time_zone
12
12
 
13
- helper_method :hr_current_user, :hr_admin?, :hr_leadership?, :hr_display_name
13
+ helper_method :hr_current_user, :hr_admin?, :hr_leadership?, :hr_superadmin?, :hr_display_name
14
14
 
15
15
  private
16
16
 
@@ -26,6 +26,10 @@ module HrLite
26
26
  HrLite.admin?(hr_current_user)
27
27
  end
28
28
 
29
+ def hr_superadmin?
30
+ HrLite.superadmin?(hr_current_user)
31
+ end
32
+
29
33
  def hr_leadership?
30
34
  HrLite.leadership?(hr_current_user)
31
35
  end
@@ -20,11 +20,15 @@ module HrLite
20
20
 
21
21
  LEADERSHIP_NAV_ITEMS = [
22
22
  { label: "Employees", path: :admin_employees_path, match: [ "/admin/employees" ] },
23
- { label: "Payroll", path: :admin_payroll_runs_path, match: [ "/admin/payroll_runs", "/admin/salary_slips" ] },
24
23
  { label: "Settings", path: :admin_leave_types_path, match: [ "/admin/leave_types", "/admin/office_locations", "/admin/holidays", "/admin/setting" ] },
25
24
  { label: "Audit", path: :admin_audit_logs_path, match: [ "/admin/audit_logs" ] }
26
25
  ].freeze
27
26
 
27
+ # Money tier — only config.superadmin_emails see these.
28
+ SUPERADMIN_NAV_ITEMS = [
29
+ { label: "Payroll", path: :admin_payroll_runs_path, match: [ "/admin/payroll_runs", "/admin/salary_slips" ] }
30
+ ].freeze
31
+
28
32
  # Only items whose routes exist yet (the nav grows with each phase).
29
33
  def hrl_nav_items(items)
30
34
  items.select { |item| hr_lite_route?(item[:path]) }
@@ -21,6 +21,8 @@ module HrLite
21
21
  # config.onboard_user; never persisted, never audited).
22
22
  attr_accessor :new_user_name, :new_user_email, :new_user_password
23
23
 
24
+ before_validation :assign_employee_code, on: :create
25
+
24
26
  validates :employee_code, presence: true, uniqueness: true
25
27
  validates :user_id, uniqueness: true
26
28
  validates :date_of_joining, presence: true
@@ -89,6 +91,25 @@ module HrLite
89
91
 
90
92
  private
91
93
 
94
+ # Codes are system-assigned: Settings prefix + zero-padded next number
95
+ # (EMP001, EMP002, ...). Scans the highest existing suffix for the
96
+ # CURRENT prefix so changing the prefix restarts a fresh sequence
97
+ # without colliding with history.
98
+ def assign_employee_code
99
+ return if employee_code.present?
100
+
101
+ prefix = Setting.instance.employee_code_prefix
102
+ last = self.class.where("employee_code LIKE ?", "#{sanitize_sql_like(prefix)}%")
103
+ .pluck(:employee_code)
104
+ .filter_map { |code| code.delete_prefix(prefix)[/\A\d+\z/]&.to_i }
105
+ .max || 0
106
+ self.employee_code = format("%s%03d", prefix, last + 1)
107
+ end
108
+
109
+ def sanitize_sql_like(value)
110
+ self.class.sanitize_sql_like(value)
111
+ end
112
+
92
113
  # A newly-assigned manager must be a real, still-employed staff member.
93
114
  def manager_is_active_staff
94
115
  return if manager_id.nil?
@@ -6,6 +6,8 @@ module HrLite
6
6
  WEEKEND_POLICIES = %w[sun_only sat_sun second_fourth_sat_sun].freeze
7
7
 
8
8
  validates :weekend_policy, inclusion: { in: WEEKEND_POLICIES }
9
+ validates :employee_code_prefix, presence: true,
10
+ format: { with: /\A[A-Za-z]{1,10}\z/, message: "letters only, max 10" }
9
11
 
10
12
  def self.instance
11
13
  first_or_create!
@@ -28,7 +28,16 @@
28
28
  </div>
29
29
  </fieldset>
30
30
  <% end %>
31
- <div class="hrl-field"><%= f.label :employee_code %><%= f.text_field :employee_code %></div>
31
+ <% if profile.persisted? %>
32
+ <div class="hrl-field">
33
+ <%= f.label :employee_code %>
34
+ <input class="hrl-input" type="text" value="<%= profile.employee_code %>" disabled>
35
+ <p class="hrl-hint">System-assigned; the prefix lives under Settings.</p>
36
+ </div>
37
+ <% else %>
38
+ <p class="hrl-hint">The employee code is assigned automatically
39
+ (<%= HrLite::Setting.instance.employee_code_prefix %> + next number).</p>
40
+ <% end %>
32
41
  <div class="hrl-field"><%= f.label :designation %><%= f.text_field :designation %></div>
33
42
  <div class="hrl-field">
34
43
  <%= f.label :manager_id, "Reports to (their L1)" %>
@@ -1,6 +1,9 @@
1
1
  <% content_for(:page_title) { "Employees" } %>
2
2
  <div class="hrl-page__head">
3
3
  <h1 class="hrl-page__title">Employees</h1>
4
+ <div class="hrl-page__actions">
5
+ <a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_admin_employee_path %>">New employee</a>
6
+ </div>
4
7
  </div>
5
8
 
6
9
  <section class="hrl-card">
@@ -3,9 +3,11 @@
3
3
  <h1 class="hrl-page__title"><%= hr_display_name(@profile.user) %> <span class="hrl-muted">(<%= @profile.employee_code %>)</span></h1>
4
4
  <div class="hrl-page__actions">
5
5
  <a class="hrl-btn" href="<%= hr_lite.edit_admin_employee_path(@profile) %>">Edit</a>
6
- <a class="hrl-btn" href="<%= hr_lite.new_admin_employee_appraisal_path(@profile) %>">New appraisal</a>
7
- <a class="hrl-btn" href="<%= hr_lite.new_admin_employee_designation_change_path(@profile) %>">Role change</a>
8
- <a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_admin_employee_salary_structure_path(@profile) %>">New structure</a>
6
+ <% if hr_superadmin? %>
7
+ <a class="hrl-btn" href="<%= hr_lite.new_admin_employee_appraisal_path(@profile) %>">New appraisal</a>
8
+ <a class="hrl-btn" href="<%= hr_lite.new_admin_employee_designation_change_path(@profile) %>">Role change</a>
9
+ <a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_admin_employee_salary_structure_path(@profile) %>">New structure</a>
10
+ <% end %>
9
11
  </div>
10
12
  </div>
11
13
 
@@ -68,6 +70,7 @@
68
70
  </dl>
69
71
  </section>
70
72
 
73
+ <% if hr_superadmin? %>
71
74
  <section class="hrl-card">
72
75
  <h2 class="hrl-card__title">Salary structures</h2>
73
76
  <% if @structures.any? %>
@@ -90,8 +93,10 @@
90
93
  <p class="hrl-muted hrl-small">No structure yet — payroll will skip this employee.</p>
91
94
  <% end %>
92
95
  </section>
96
+ <% end %>
93
97
  </div>
94
98
 
99
+ <% if hr_superadmin? %>
95
100
  <section class="hrl-card">
96
101
  <h2 class="hrl-card__title">Appraisals</h2>
97
102
  <% appraisals = HrLite::Appraisal.where(user_id: @profile.user_id).recent_first %>
@@ -116,3 +121,4 @@
116
121
  <p class="hrl-muted hrl-small">No appraisals yet.</p>
117
122
  <% end %>
118
123
  </section>
124
+ <% end %>
@@ -11,6 +11,13 @@
11
11
  <%= f.select :weekend_policy, HrLite::Setting::WEEKEND_POLICIES.map { |p| [ p.humanize, p ] } %>
12
12
  <p class="hrl-hint">Drives working-day math everywhere: leave counting, attendance, payroll.</p>
13
13
  </div>
14
+ <div class="hrl-field">
15
+ <%= f.label :employee_code_prefix, "Employee code prefix" %>
16
+ <%= f.text_field :employee_code_prefix %>
17
+ <p class="hrl-hint">Codes are assigned automatically as
18
+ <%= @setting.employee_code_prefix %>001, <%= @setting.employee_code_prefix %>002, …
19
+ — you only choose the prefix.</p>
20
+ </div>
14
21
  <div class="hrl-form-actions">
15
22
  <%= f.submit "Save", class: "hrl-btn hrl-btn--primary" %>
16
23
  </div>
@@ -42,6 +42,11 @@
42
42
  <% hrl_nav_items(HrLite::ApplicationHelper::LEADERSHIP_NAV_ITEMS).each do |item| %>
43
43
  <%= hrl_nav_link(item) %>
44
44
  <% end %>
45
+ <% if hr_superadmin? %>
46
+ <% hrl_nav_items(HrLite::ApplicationHelper::SUPERADMIN_NAV_ITEMS).each do |item| %>
47
+ <%= hrl_nav_link(item) %>
48
+ <% end %>
49
+ <% end %>
45
50
  <% end %>
46
51
  </nav>
47
52
 
@@ -0,0 +1,7 @@
1
+ class AddEmployeeCodePrefixToHrLiteSettings < ActiveRecord::Migration[8.0]
2
+ def change
3
+ # Employee codes are system-assigned: prefix + zero-padded sequence
4
+ # (EMP001, EMP002, ...). Leadership edits the prefix under Settings.
5
+ add_column :hr_lite_settings, :employee_code_prefix, :string, null: false, default: "EMP"
6
+ end
7
+ end
@@ -8,6 +8,7 @@ module HrLite
8
8
  :employees_scope, :mentionable_users, :notify, :render_pdf, :company,
9
9
  :time_zone, :currency_symbol, :on_designation_change,
10
10
  :leadership_emails, :leadership_check, :extra_stylesheets,
11
+ :superadmin_emails, :superadmin_check,
11
12
  :mailer_from, :public_url_base, :notification_matrix, :back_link,
12
13
  :onboard_user, :offboard_user, :invite_url_for
13
14
 
@@ -49,6 +50,13 @@ module HrLite
49
50
  emails = HrLite.config.leadership_emails.map { |e| e.to_s.downcase.strip }
50
51
  emails.include?(user.email.to_s.downcase)
51
52
  end
53
+ # Money tier: salary structures, payroll, slips, appraisals. Empty
54
+ # list means "same as leadership" (pre-0.5.0 behaviour).
55
+ @superadmin_emails = []
56
+ @superadmin_check = ->(user) do
57
+ emails = HrLite.config.superadmin_emails.map { |e| e.to_s.downcase.strip }.reject(&:empty?)
58
+ emails.empty? ? HrLite.leadership?(user) : emails.include?(user.email.to_s.downcase)
59
+ end
52
60
  @extra_stylesheets = [] # host stylesheets linked AFTER hr_lite.css (CSS-var overrides)
53
61
  @mailer_from = "hr@example.com"
54
62
  @public_url_base = nil # e.g. "https://hr.example.com" — enables email links + HrLite.public_url
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
data/lib/hr_lite.rb CHANGED
@@ -38,6 +38,10 @@ module HrLite
38
38
  user.present? && !!config.leadership_check.call(user)
39
39
  end
40
40
 
41
+ def superadmin?(user)
42
+ user.present? && !!config.superadmin_check.call(user)
43
+ end
44
+
41
45
  # Leadership members resolvable to actual user records (for bell
42
46
  # notifications). Emails configured but absent from the user table are
43
47
  # still reachable by email — see Notifications.
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.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kshitiz sinha
@@ -74,6 +74,7 @@ files:
74
74
  - app/controllers/hr_lite/admin/salary_slips_controller.rb
75
75
  - app/controllers/hr_lite/admin/salary_structures_controller.rb
76
76
  - app/controllers/hr_lite/admin/settings_controller.rb
77
+ - app/controllers/hr_lite/admin/superadmin_controller.rb
77
78
  - app/controllers/hr_lite/application_controller.rb
78
79
  - app/controllers/hr_lite/appraisals_controller.rb
79
80
  - app/controllers/hr_lite/attendance_controller.rb
@@ -236,6 +237,7 @@ files:
236
237
  - db/migrate/20260719104316_create_hr_lite_comp_off_requests.rb
237
238
  - db/migrate/20260719104317_create_hr_lite_regularization_requests.rb
238
239
  - db/migrate/20260719162154_add_manager_to_hr_lite_employee_profiles.rb
240
+ - db/migrate/20260720094454_add_employee_code_prefix_to_hr_lite_settings.rb
239
241
  - lib/generators/hr_lite/install/install_generator.rb
240
242
  - lib/generators/hr_lite/install/templates/AFTER_INSTALL
241
243
  - lib/generators/hr_lite/install/templates/initializer.rb