hr_lite 0.3.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/app/assets/stylesheets/hr_lite/hr_lite.css +18 -0
  4. data/app/controllers/hr_lite/admin/appraisals_controller.rb +1 -1
  5. data/app/controllers/hr_lite/admin/designation_changes_controller.rb +1 -1
  6. data/app/controllers/hr_lite/admin/employees_controller.rb +1 -1
  7. data/app/controllers/hr_lite/admin/leave_balances_controller.rb +1 -1
  8. data/app/controllers/hr_lite/admin/payroll_runs_controller.rb +1 -1
  9. data/app/controllers/hr_lite/admin/salary_slips_controller.rb +1 -1
  10. data/app/controllers/hr_lite/admin/salary_structures_controller.rb +1 -1
  11. data/app/controllers/hr_lite/admin/settings_controller.rb +1 -1
  12. data/app/controllers/hr_lite/admin/superadmin_controller.rb +16 -0
  13. data/app/controllers/hr_lite/application_controller.rb +5 -1
  14. data/app/controllers/hr_lite/leave_balances_controller.rb +1 -1
  15. data/app/controllers/hr_lite/leave_requests_controller.rb +1 -1
  16. data/app/controllers/hr_lite/org_controller.rb +51 -0
  17. data/app/helpers/hr_lite/application_helper.rb +6 -1
  18. data/app/jobs/hr_lite/leave_year_rollover_job.rb +18 -6
  19. data/app/models/hr_lite/comp_off_request.rb +6 -6
  20. data/app/models/hr_lite/employee_profile.rb +76 -0
  21. data/app/models/hr_lite/leave_balance.rb +43 -10
  22. data/app/models/hr_lite/leave_request.rb +5 -5
  23. data/app/models/hr_lite/setting.rb +2 -0
  24. data/app/views/hr_lite/admin/employees/_form.html.erb +17 -1
  25. data/app/views/hr_lite/admin/employees/index.html.erb +3 -0
  26. data/app/views/hr_lite/admin/employees/show.html.erb +10 -3
  27. data/app/views/hr_lite/admin/leave_balances/index.html.erb +1 -1
  28. data/app/views/hr_lite/admin/settings/edit.html.erb +7 -0
  29. data/app/views/hr_lite/employee_profiles/show.html.erb +3 -0
  30. data/app/views/hr_lite/leave_balances/index.html.erb +1 -1
  31. data/app/views/hr_lite/leave_requests/_balance_chips.html.erb +2 -2
  32. data/app/views/hr_lite/org/_node.html.erb +14 -0
  33. data/app/views/hr_lite/org/show.html.erb +38 -0
  34. data/app/views/layouts/hr_lite/application.html.erb +5 -0
  35. data/config/routes.rb +1 -0
  36. data/db/migrate/20260719162154_add_manager_to_hr_lite_employee_profiles.rb +8 -0
  37. data/db/migrate/20260720094454_add_employee_code_prefix_to_hr_lite_settings.rb +7 -0
  38. data/lib/hr_lite/configuration.rb +22 -0
  39. data/lib/hr_lite/leave_year.rb +33 -0
  40. data/lib/hr_lite/version.rb +1 -1
  41. data/lib/hr_lite.rb +5 -0
  42. metadata +8 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4ea6b78faec5dd0012db2be857b1874707c5085e99c54114d611893122be1a0
4
- data.tar.gz: 0572af1000548da1d1d66699939360827dd8ef6fa1cc420ed236dfb90fd69a25
3
+ metadata.gz: 00cc5c346c88ddc803a5336a1c14a07dd4609fc887213f99e9b11654f26fa109
4
+ data.tar.gz: 8306411151a38ea5b1fe0fe9c7b4bd3572e16917a1defc1e15d4e823ab7f9c1b
5
5
  SHA512:
6
- metadata.gz: 79300a2187ebae2eaa26a51a4c0d8845a380cfdb6787dd2df0e3d54a18a0da32b2668586f4cd2c4f4c5d5644ff7c4a5979ab60faf69c94c8a6ee1db7bf18f91a
7
- data.tar.gz: 91442183160220175e78f0951917da321b4c555f1384772dc08cbc9afc48c7ae0fdaaa7d59591181b72bb8358a75b4e20f941283860b1a755b8aa5eddc1e3729
6
+ metadata.gz: 6f9be2057c4023881d956fc8feadb18d2917bb38cd92746b7a448a60fedea0fc94d335b22d36a627a8ce888e36a7520ca6118ad77e6f5956b96fdec695f813dc
7
+ data.tar.gz: 95c8048ecd82dc4b92d9e4e5b79240bed1385e297206383280c76e010d141f666e57eb150eac9defdbe556978226d7b4a5fff21ff8132103e04e09862a36f3ad
data/CHANGELOG.md CHANGED
@@ -7,6 +7,61 @@ 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
+
26
+ ## [0.4.0] - 2026-07-19
27
+
28
+ ### Added
29
+
30
+ - **Org chart (`/org`)**: everyone-visible reporting tree (who reports to
31
+ whom, names + designations + departments only — never salary or private
32
+ data) plus each viewer's own reporting line labelled L1/L2/... Managers
33
+ are set per employee by leadership ("Reports to"); reporting loops are
34
+ rejected.
35
+ - **Configurable leave year** (`config.leave_year_start_month`, default 1):
36
+ set 7 for a July–June leave year. Balances, accrual, the year-boundary
37
+ split rule, carry-forward rollover and comp-off credits all follow it.
38
+ Balance headings show "2026–27"-style labels for non-calendar years.
39
+ - **Joining-date proration, Keka-style**: entitlement accrues only from
40
+ the month someone joins (joined on/before the 15th → that month counts;
41
+ after → from the next month). Applies to monthly accrual AND
42
+ yearly-upfront grants (upfront = remaining months × monthly rate).
43
+
44
+ ### Changed
45
+
46
+ - `LeaveBalance#year` is now a LeaveYear key; `LeaveYearRolloverJob`
47
+ defaults to the current leave year in the configured HR time zone
48
+ (schedule it on the leave year's first day). The request-split
49
+ validation message says "leave-year boundary".
50
+
51
+ ### Upgrade notes
52
+
53
+ - **Set `leave_year_start_month` once, at install time.** Balance rows
54
+ are keyed by leave year with no stored epoch — changing the start
55
+ month on an install with existing balances silently reinterprets
56
+ every row (carry, adjustments, comp-off credits) and miskeys
57
+ historical requests. The setter validates 1..12 and accepts "7".
58
+ - **Joining-date proration applies to existing data.** Employees who
59
+ joined partway through the CURRENT year previously showed the full
60
+ year's accrual; from 0.4.0 they accrue only from their joining month,
61
+ so their entitlement can drop (and, if they already used more, go
62
+ negative). Where the old number was intended, add a one-off balance
63
+ adjustment with a note.
64
+
10
65
  ## [0.3.0] - 2026-07-19
11
66
 
12
67
  ### Added
@@ -241,6 +241,24 @@
241
241
  .hrl-pager { display: flex; align-items: center; justify-content: center; gap: .8rem; margin: 1rem 0; font-size: .88rem; }
242
242
  .hrl-pager__info { color: var(--hrl-muted); }
243
243
 
244
+ /* Org chart tree + reporting-line chain */
245
+ .hrl-tree { list-style: none; margin: 0; padding-left: 0; }
246
+ .hrl-tree .hrl-tree { padding-left: 1.1rem; border-left: 2px solid var(--hrl-line); margin-left: .55rem; }
247
+ .hrl-tree__node { margin: .3rem 0; }
248
+ .hrl-tree__card {
249
+ display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: .45rem;
250
+ background: var(--hrl-bg); border: 1px solid var(--hrl-line);
251
+ border-radius: var(--hrl-radius-sm); padding: .4rem .7rem; max-width: 100%;
252
+ }
253
+ .hrl-tree__card--me { border-color: var(--hrl-accent); background: var(--hrl-info-bg); }
254
+ .hrl-tree__name, .hrl-tree__role { min-width: 0; overflow-wrap: anywhere; }
255
+ .hrl-tree__name { font-weight: 650; font-size: .9rem; }
256
+ .hrl-tree__role { color: var(--hrl-muted); font-size: .8rem; }
257
+ .hrl-chainrow { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
258
+ .hrl-chainrow__step { display: inline-flex; align-items: center; gap: .4rem; min-width: 0; }
259
+ .hrl-chainrow .hrl-badge { white-space: normal; overflow-wrap: anywhere; }
260
+ .hrl-chainrow__arrow { color: var(--hrl-muted); }
261
+
244
262
  /* Definition list */
245
263
  .hrl-deflist { display: grid; grid-template-columns: minmax(7rem, auto) 1fr; gap: .35rem .9rem; font-size: .9rem; }
246
264
  .hrl-deflist dt { color: var(--hrl-muted); }
@@ -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, :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
  )
@@ -3,7 +3,7 @@ module HrLite
3
3
  class LeaveBalancesController < BaseController
4
4
  def index
5
5
  year = params[:year].to_i
6
- @year = year.between?(2000, 2100) ? year : Date.current.year
6
+ @year = year.between?(2000, 2100) ? year : LeaveYear.current_key
7
7
  @types = LeaveType.active.where(paid: true).where.not(annual_quota: nil)
8
8
  @employees = HrLite.employees
9
9
  end
@@ -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
@@ -2,7 +2,7 @@ module HrLite
2
2
  class LeaveBalancesController < ApplicationController
3
3
  def index
4
4
  year = params[:year].to_i
5
- @year = year.between?(2000, 2100) ? year : Date.current.year
5
+ @year = year.between?(2000, 2100) ? year : LeaveYear.current_key
6
6
  @balances = LeaveType.active.where(paid: true).where.not(annual_quota: nil).map do |type|
7
7
  LeaveBalance.for(hr_current_user, type, @year)
8
8
  end
@@ -43,7 +43,7 @@ module HrLite
43
43
  end
44
44
 
45
45
  def balance_cards
46
- year = Date.current.year
46
+ year = LeaveYear.current_key
47
47
  LeaveType.active.where(paid: true).where.not(annual_quota: nil).map do |type|
48
48
  LeaveBalance.for(hr_current_user, type, year)
49
49
  end
@@ -0,0 +1,51 @@
1
+ module HrLite
2
+ # The everyone-visible org chart: who reports to whom, plus the viewer's
3
+ # own reporting line labelled L1/L2/... Only names, designations and
4
+ # departments — never salary, identity numbers or any other private data.
5
+ class OrgController < ApplicationController
6
+ def show
7
+ profiles = EmployeeProfile.includes(:user)
8
+ .where("date_of_exit IS NULL OR date_of_exit >= ?", Date.current)
9
+ .to_a
10
+ @by_user = profiles.index_by(&:user_id)
11
+ build_tree(profiles)
12
+ @own_profile = @by_user[hr_current_user.id]
13
+ exited = EmployeeProfile.where(date_of_exit: ...Date.current).pluck(:user_id).to_set
14
+ @own_chain = (@own_profile&.reporting_chain || []).reject { |boss| exited.include?(boss.id) }
15
+ end
16
+
17
+ private
18
+
19
+ # BFS from the natural roots, then promote anything unreached (a
20
+ # manager cycle that slipped past validation must degrade to extra
21
+ # roots, not silently vanish). @tree_children only ever contains
22
+ # forward edges, so the recursive partial always terminates and every
23
+ # active profile renders exactly once.
24
+ def build_tree(profiles)
25
+ children = profiles.group_by(&:manager_id)
26
+ @roots = profiles.select { |p| p.manager_id.nil? || !@by_user.key?(p.manager_id) }
27
+ .sort_by { |p| HrLite.display_name(p.user).downcase }
28
+ @tree_children = Hash.new { |hash, key| hash[key] = [] }
29
+ visited = {}
30
+ walk = lambda do |node|
31
+ (children[node.user_id] || []).each do |child|
32
+ next if visited[child.user_id]
33
+
34
+ visited[child.user_id] = true
35
+ @tree_children[node.user_id] << child
36
+ walk.call(child)
37
+ end
38
+ end
39
+ @roots.each { |root| visited[root.user_id] = true }
40
+ @roots.each { |root| walk.call(root) }
41
+
42
+ profiles.sort_by(&:id).each do |profile|
43
+ next if visited[profile.user_id]
44
+
45
+ visited[profile.user_id] = true
46
+ @roots << profile
47
+ walk.call(profile)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -6,6 +6,7 @@ module HrLite
6
6
  { label: "Leaves", path: :leave_requests_path, match: [ "/leave_requests", "/leave_balances", "/comp_off_requests" ] },
7
7
  { label: "Team", path: :team_path, match: [ "/team" ] },
8
8
  { label: "Calendar", path: :calendar_path, match: [ "/calendar", "/holidays" ] },
9
+ { label: "Org", path: :org_path, match: [ "/org" ] },
9
10
  { label: "Kudos", path: :kudos_path, match: [ "/kudos" ] },
10
11
  { label: "Slips", path: :salary_slips_path, match: [ "/salary_slips" ] },
11
12
  { label: "Career", path: :career_path, match: [ "/career", "/appraisals", "/profile" ] }
@@ -19,11 +20,15 @@ module HrLite
19
20
 
20
21
  LEADERSHIP_NAV_ITEMS = [
21
22
  { label: "Employees", path: :admin_employees_path, match: [ "/admin/employees" ] },
22
- { label: "Payroll", path: :admin_payroll_runs_path, match: [ "/admin/payroll_runs", "/admin/salary_slips" ] },
23
23
  { label: "Settings", path: :admin_leave_types_path, match: [ "/admin/leave_types", "/admin/office_locations", "/admin/holidays", "/admin/setting" ] },
24
24
  { label: "Audit", path: :admin_audit_logs_path, match: [ "/admin/audit_logs" ] }
25
25
  ].freeze
26
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
+
27
32
  # Only items whose routes exist yet (the nav grows with each phase).
28
33
  def hrl_nav_items(items)
29
34
  items.select { |item| hr_lite_route?(item[:path]) }
@@ -1,13 +1,25 @@
1
1
  module HrLite
2
- # Jan-1 rollover: materialize carry-forward into the new year's balance
3
- # rows. Idempotent a balance whose carry has already been written is
4
- # never touched again (manual adjustments stay safe).
2
+ # Leave-year rollover (host schedules it on the leave year's first day —
3
+ # Jan 1 for calendar years, Jul 1 for July–June years): materialize
4
+ # carry-forward into the new year's balance rows. Idempotent — a balance
5
+ # whose carry has already been written is never touched again (manual
6
+ # adjustments stay safe).
5
7
  class LeaveYearRolloverJob < ActiveJob::Base
6
8
  queue_as :default
7
9
 
8
- def perform(year: Date.current.year)
9
- previous_year = year - 1
10
+ def perform(year: nil)
11
+ # Resolve "today" in the HR time zone — cron often fires around the
12
+ # boundary midnight, and the host process may run in UTC.
13
+ Time.use_zone(HrLite.config.time_zone) do
14
+ year ||= LeaveYear.current_key
15
+ previous_year = year - 1
16
+ roll(year, previous_year)
17
+ end
18
+ end
19
+
20
+ private
10
21
 
22
+ def roll(year, previous_year)
11
23
  LeaveType.active.where(paid: true).where.not(annual_quota: nil).find_each do |type|
12
24
  next unless type.carry_forward_cap.positive?
13
25
 
@@ -16,7 +28,7 @@ module HrLite
16
28
  next if balance.persisted? && balance.carried_forward.positive?
17
29
 
18
30
  carry = LeaveBalance.for(user, type, previous_year)
19
- .available(as_of: Date.new(previous_year, 12, 31))
31
+ .available(as_of: LeaveYear.range(previous_year).last)
20
32
  carry = [ [ carry, type.carry_forward_cap ].min, 0 ].max
21
33
  next if carry.zero? && balance.persisted?
22
34
 
@@ -100,13 +100,13 @@ module HrLite
100
100
 
101
101
  private
102
102
 
103
- # The credit goes into the year it can actually be SPENT (the current
104
- # year at approval time) — crediting date_worked.year would strand a
105
- # late-December day approved in January on last year's dead balance.
106
- # The balance row is locked for the increment; two admins approving two
107
- # different requests for the same person cannot lose an update.
103
+ # The credit goes into the leave year it can actually be SPENT (the
104
+ # current one at approval time) — crediting date_worked's leave year
105
+ # would strand a year-end day approved after rollover on a dead
106
+ # balance. The balance row is locked for the increment; two admins
107
+ # approving two different requests for one person cannot lose an update.
108
108
  def credit_balance!(type)
109
- year = [ date_worked.year, Date.current.year ].max
109
+ year = LeaveYear.current_key
110
110
  balance = LeaveBalance.for(user, type, year)
111
111
  if balance.new_record?
112
112
  begin
@@ -8,6 +8,9 @@ module HrLite
8
8
  include Audited
9
9
 
10
10
  belongs_to :user, class_name: HrLite.config.user_class
11
+ # Reporting line (L1). Optional; the org chart treats manager-less
12
+ # profiles as roots (founders/directors).
13
+ belongs_to :manager, class_name: HrLite.config.user_class, optional: true
11
14
 
12
15
  encrypts :pan_number, :pf_uan, :esi_number, :bank_account_number, :bank_ifsc
13
16
  encrypted_money :declared_annual_deductions
@@ -18,6 +21,8 @@ module HrLite
18
21
  # config.onboard_user; never persisted, never audited).
19
22
  attr_accessor :new_user_name, :new_user_email, :new_user_password
20
23
 
24
+ before_validation :assign_employee_code, on: :create
25
+
21
26
  validates :employee_code, presence: true, uniqueness: true
22
27
  validates :user_id, uniqueness: true
23
28
  validates :date_of_joining, presence: true
@@ -28,6 +33,8 @@ module HrLite
28
33
  allow_blank: true
29
34
  validates :pf_uan, format: { with: /\A\d{12}\z/, message: "must be 12 digits" }, allow_blank: true
30
35
  validate :exit_after_joining
36
+ validate :manager_chain_acyclic
37
+ validate :manager_is_active_staff, if: :manager_id_changed?
31
38
 
32
39
  scope :active_for, ->(month) {
33
40
  where(date_of_joining: ..month.end_of_month)
@@ -44,6 +51,31 @@ module HrLite
44
51
  from <= to ? (from..to) : nil
45
52
  end
46
53
 
54
+ # The manager as shown to the employee — an exited manager reads as
55
+ # "no manager" until leadership reassigns (matches the org chart).
56
+ def active_manager
57
+ return nil if manager.nil?
58
+
59
+ boss_exit = EmployeeProfile.where(user_id: manager_id).pick(:date_of_exit)
60
+ boss_exit && boss_exit < Date.current ? nil : manager
61
+ end
62
+
63
+ # [L1 user, L2 user, ...] walking manager_id upward. Cycle-safe.
64
+ def reporting_chain
65
+ chain = []
66
+ seen = { user_id => true }
67
+ current = manager_id
68
+ while current && !seen[current]
69
+ seen[current] = true
70
+ boss = HrLite.user_klass.find_by(id: current)
71
+ break if boss.nil?
72
+
73
+ chain << boss
74
+ current = EmployeeProfile.where(user_id: boss.id).pick(:manager_id)
75
+ end
76
+ chain
77
+ end
78
+
47
79
  def masked_pan
48
80
  mask_middle(pan_number)
49
81
  end
@@ -59,6 +91,50 @@ module HrLite
59
91
 
60
92
  private
61
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
+
113
+ # A newly-assigned manager must be a real, still-employed staff member.
114
+ def manager_is_active_staff
115
+ return if manager_id.nil?
116
+ return errors.add(:manager_id, "is not a staff account") if HrLite.user_klass.find_by(id: manager_id).nil?
117
+
118
+ boss_exit = EmployeeProfile.where(user_id: manager_id).pick(:date_of_exit)
119
+ errors.add(:manager_id, "has already exited") if boss_exit && boss_exit < Date.current
120
+ end
121
+
122
+ # Walking up from the proposed manager must never reach this person.
123
+ def manager_chain_acyclic
124
+ return if manager_id.nil?
125
+ return errors.add(:manager_id, "cannot be yourself") if manager_id == user_id
126
+
127
+ seen = {}
128
+ current = manager_id
129
+ while current
130
+ return errors.add(:manager_id, "creates a reporting loop") if current == user_id
131
+ break if seen[current]
132
+
133
+ seen[current] = true
134
+ current = EmployeeProfile.where(user_id: current).pick(:manager_id)
135
+ end
136
+ end
137
+
62
138
  def mask_middle(value)
63
139
  return nil if value.blank?
64
140
  return "•" * value.length if value.length <= 4
@@ -3,6 +3,11 @@ module HrLite
3
3
  # entitlement accrues as a pure function of the policy and `used` is
4
4
  # recomputed live from approved requests — so a holiday added after an
5
5
  # approval self-heals both the quota and payroll.
6
+ #
7
+ # `year` is a LeaveYear key (calendar year by default; with
8
+ # config.leave_year_start_month = 7, key 2026 = Jul 2026 – Jun 2027).
9
+ # Entitlement prorates from the joining date, Keka-style: joined on or
10
+ # before the 15th → that month counts; after the 15th → from next month.
6
11
  class LeaveBalance < ApplicationRecord
7
12
  belongs_to :user, class_name: HrLite.config.user_class
8
13
  belongs_to :leave_type
@@ -17,26 +22,54 @@ module HrLite
17
22
  def entitled(as_of: Date.current)
18
23
  return Float::INFINITY if leave_type.unlimited?
19
24
 
20
- quota = leave_type.annual_quota
21
- base =
22
- if leave_type.accrual == "monthly"
23
- months = as_of.year == year ? as_of.month : (as_of.year > year ? 12 : 0)
24
- ((quota / 12) * months).round(1)
25
- else
26
- quota
27
- end
28
- base + carried_forward + adjustment
25
+ (accrued_base(as_of) + carried_forward + adjustment).round(1)
29
26
  end
30
27
 
31
28
  def used
29
+ range = LeaveYear.range(year)
32
30
  requests = LeaveRequest.approved
33
31
  .where(user_id: user_id, leave_type_id: leave_type_id)
34
- .where(start_date: Date.new(year, 1, 1)..Date.new(year, 12, 31))
32
+ .where(start_date: range)
35
33
  requests.sum { |request| LeaveDayCounter.count(request) }
36
34
  end
37
35
 
38
36
  def available(as_of: Date.current)
39
37
  entitled(as_of: as_of) - used
40
38
  end
39
+
40
+ private
41
+
42
+ # Quota earned by as_of, before carry/adjustments. Monthly accrual
43
+ # drips quota/12 per month from the accrual start (leave-year start,
44
+ # or the prorated joining month for mid-year joiners); yearly_upfront
45
+ # grants all months from the accrual start to the year's end at once.
46
+ def accrued_base(as_of)
47
+ range = LeaveYear.range(year)
48
+ start = accrual_start(range)
49
+ return 0 if start.nil?
50
+
51
+ monthly_rate = leave_type.annual_quota / 12
52
+ months =
53
+ if leave_type.accrual == "monthly"
54
+ cap = [ as_of, range.last ].min
55
+ start > cap ? 0 : months_between(start, cap)
56
+ else
57
+ months_between(start, range.last)
58
+ end
59
+ monthly_rate * months
60
+ end
61
+
62
+ # nil when the person joins only after this leave year ends.
63
+ def accrual_start(range)
64
+ doj = EmployeeProfile.where(user_id: user_id).pick(:date_of_joining)
65
+ return range.first if doj.nil? || doj <= range.first
66
+
67
+ start = doj.day <= 15 ? doj.beginning_of_month : doj.next_month.beginning_of_month
68
+ start > range.last ? nil : start
69
+ end
70
+
71
+ def months_between(from, to)
72
+ (to.year * 12 + to.month) - (from.year * 12 + from.month) + 1
73
+ end
41
74
  end
42
75
  end
@@ -11,7 +11,7 @@ module HrLite
11
11
 
12
12
  with_options on: :create do
13
13
  validate :end_after_start
14
- validate :same_calendar_year
14
+ validate :same_leave_year
15
15
  validate :half_day_single_day_only
16
16
  validate :consumes_at_least_half_a_day
17
17
  validate :no_overlap_with_own_requests
@@ -97,7 +97,7 @@ module HrLite
97
97
  end
98
98
 
99
99
  def balance
100
- LeaveBalance.for(user, leave_type, start_date.year)
100
+ LeaveBalance.for(user, leave_type, LeaveYear.key_for(start_date))
101
101
  end
102
102
 
103
103
  private
@@ -172,11 +172,11 @@ module HrLite
172
172
  errors.add(:end_date, "must be on or after the start date") if end_date < start_date
173
173
  end
174
174
 
175
- def same_calendar_year
175
+ def same_leave_year
176
176
  return unless start_date && end_date
177
177
 
178
- if start_date.year != end_date.year
179
- errors.add(:base, "Split requests at the year boundary")
178
+ if LeaveYear.key_for(start_date) != LeaveYear.key_for(end_date)
179
+ errors.add(:base, "Split requests at the leave-year boundary")
180
180
  end
181
181
  end
182
182
 
@@ -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,8 +28,24 @@
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>
42
+ <div class="hrl-field">
43
+ <%= f.label :manager_id, "Reports to (their L1)" %>
44
+ <%= f.select :manager_id,
45
+ HrLite.active_employees.reject { |u| u.id == profile.user_id }
46
+ .map { |u| [ HrLite.display_name(u), u.id ] },
47
+ include_blank: "— nobody (top of the org) —" %>
48
+ </div>
33
49
  <div class="hrl-field"><%= f.label :department %><%= f.text_field :department %></div>
34
50
  <div class="hrl-field"><%= f.label :work_location %><%= f.text_field :work_location %></div>
35
51
  <div class="hrl-field"><%= f.label :date_of_birth %><%= f.date_field :date_of_birth %></div>
@@ -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
 
@@ -56,6 +58,7 @@
56
58
  <h2 class="hrl-card__title">Profile</h2>
57
59
  <dl class="hrl-deflist">
58
60
  <dt>Designation</dt><dd><%= @profile.designation.presence || "—" %></dd>
61
+ <dt>Reports to</dt><dd><%= @profile.manager ? hr_display_name(@profile.manager) : "—" %></dd>
59
62
  <dt>Department</dt><dd><%= @profile.department.presence || "—" %></dd>
60
63
  <dt>Joined</dt><dd><%= @profile.date_of_joining.strftime("%d %b %Y") %></dd>
61
64
  <% if @profile.date_of_exit %><dt>Exited</dt><dd><%= @profile.date_of_exit.strftime("%d %b %Y") %></dd><% end %>
@@ -67,6 +70,7 @@
67
70
  </dl>
68
71
  </section>
69
72
 
73
+ <% if hr_superadmin? %>
70
74
  <section class="hrl-card">
71
75
  <h2 class="hrl-card__title">Salary structures</h2>
72
76
  <% if @structures.any? %>
@@ -89,8 +93,10 @@
89
93
  <p class="hrl-muted hrl-small">No structure yet — payroll will skip this employee.</p>
90
94
  <% end %>
91
95
  </section>
96
+ <% end %>
92
97
  </div>
93
98
 
99
+ <% if hr_superadmin? %>
94
100
  <section class="hrl-card">
95
101
  <h2 class="hrl-card__title">Appraisals</h2>
96
102
  <% appraisals = HrLite::Appraisal.where(user_id: @profile.user_id).recent_first %>
@@ -115,3 +121,4 @@
115
121
  <p class="hrl-muted hrl-small">No appraisals yet.</p>
116
122
  <% end %>
117
123
  </section>
124
+ <% end %>
@@ -1,6 +1,6 @@
1
1
  <% content_for(:page_title) { "Team balances" } %>
2
2
  <div class="hrl-page__head">
3
- <h1 class="hrl-page__title">Team leave balances <%= @year %></h1>
3
+ <h1 class="hrl-page__title">Team leave balances <%= HrLite::LeaveYear.label(@year) %></h1>
4
4
  </div>
5
5
 
6
6
  <section class="hrl-card">
@@ -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>
@@ -8,6 +8,9 @@
8
8
  <dl class="hrl-deflist">
9
9
  <dt>Employee code</dt><dd><%= @profile.employee_code %></dd>
10
10
  <dt>Designation</dt><dd><%= @profile.designation.presence || "—" %></dd>
11
+ <dt>Reports to</dt>
12
+ <dd><%= (boss = @profile.active_manager) ? "#{hr_display_name(boss)} (your L1)" : "—" %>
13
+ · <a href="<%= hr_lite.org_path %>">Org chart</a></dd>
11
14
  <dt>Department</dt><dd><%= @profile.department.presence || "—" %></dd>
12
15
  <dt>Joined</dt><dd><%= @profile.date_of_joining.strftime("%d %B %Y") %></dd>
13
16
  <dt>Tax regime</dt><dd><%= @profile.tax_regime.humanize %></dd>
@@ -1,6 +1,6 @@
1
1
  <% content_for(:page_title) { "Leave balances" } %>
2
2
  <div class="hrl-page__head">
3
- <h1 class="hrl-page__title">Leave balances <%= @year %></h1>
3
+ <h1 class="hrl-page__title">Leave balances <%= HrLite::LeaveYear.label(@year) %></h1>
4
4
  </div>
5
5
 
6
6
  <div class="hrl-grid hrl-grid--2">
@@ -1,10 +1,10 @@
1
1
  <section class="hrl-card">
2
- <h2 class="hrl-card__title">Leave balance <%= Date.current.year %></h2>
2
+ <h2 class="hrl-card__title">Leave balance <%= HrLite::LeaveYear.label(HrLite::LeaveYear.current_key) %></h2>
3
3
  <% types = HrLite::LeaveType.active.where(paid: true).where.not(annual_quota: nil) %>
4
4
  <% if types.any? %>
5
5
  <div class="hrl-row">
6
6
  <% types.each do |type| %>
7
- <% balance = HrLite::LeaveBalance.for(hr_current_user, type, Date.current.year) %>
7
+ <% balance = HrLite::LeaveBalance.for(hr_current_user, type, HrLite::LeaveYear.current_key) %>
8
8
  <span class="hrl-badge" style="background: <%= type.color %>1f; color: <%= type.color %>;">
9
9
  <%= type.code %> <%= balance.available.to_f %>/<%= balance.entitled.to_f %>
10
10
  </span>
@@ -0,0 +1,14 @@
1
+ <%# locals: profile — recursive org-chart node; @children / hr_current_user from OrgController %>
2
+ <li class="hrl-tree__node">
3
+ <div class="hrl-tree__card <%= 'hrl-tree__card--me' if profile.user_id == hr_current_user.id %>">
4
+ <span class="hrl-tree__name"><%= hr_display_name(profile.user) %></span>
5
+ <% if profile.designation.present? %><span class="hrl-tree__role"><%= profile.designation %></span><% end %>
6
+ <% if profile.department.present? %><span class="hrl-small hrl-muted"><%= profile.department %></span><% end %>
7
+ </div>
8
+ <% reports = @tree_children[profile.user_id].sort_by { |child| HrLite.display_name(child.user).downcase } %>
9
+ <% if reports.any? %>
10
+ <ul class="hrl-tree">
11
+ <%= render partial: "hr_lite/org/node", collection: reports, as: :profile %>
12
+ </ul>
13
+ <% end %>
14
+ </li>
@@ -0,0 +1,38 @@
1
+ <% content_for(:page_title) { "Org chart" } %>
2
+ <div class="hrl-page__head">
3
+ <h1 class="hrl-page__title">Organisation</h1>
4
+ </div>
5
+
6
+ <% if @own_chain.any? %>
7
+ <section class="hrl-card">
8
+ <h2 class="hrl-card__title">Your reporting line</h2>
9
+ <div class="hrl-chainrow">
10
+ <span class="hrl-badge hrl-badge--ok">You</span>
11
+ <% @own_chain.each_with_index do |boss, index| %>
12
+ <span class="hrl-chainrow__step">
13
+ <span class="hrl-chainrow__arrow">&rarr;</span>
14
+ <span class="hrl-badge">
15
+ L<%= index + 1 %> · <%= hr_display_name(boss) %><% if (p = @by_user[boss.id]) && p.designation.present? %> (<%= p.designation %>)<% end %>
16
+ </span>
17
+ </span>
18
+ <% end %>
19
+ </div>
20
+ <p class="hrl-hint">L1 is your direct manager; the chain ends at the top of the organisation.</p>
21
+ </section>
22
+ <% elsif @own_profile %>
23
+ <section class="hrl-card">
24
+ <h2 class="hrl-card__title">Your reporting line</h2>
25
+ <p class="hrl-small hrl-muted">You are at the top of the organisation.</p>
26
+ </section>
27
+ <% end %>
28
+
29
+ <section class="hrl-card">
30
+ <h2 class="hrl-card__title">Who reports to whom</h2>
31
+ <% if @roots.any? %>
32
+ <ul class="hrl-tree">
33
+ <%= render partial: "hr_lite/org/node", collection: @roots, as: :profile %>
34
+ </ul>
35
+ <% else %>
36
+ <p class="hrl-card--empty">No employee profiles yet — leadership adds them under Employees.</p>
37
+ <% end %>
38
+ </section>
@@ -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
 
data/config/routes.rb CHANGED
@@ -20,6 +20,7 @@ HrLite::Engine.routes.draw do
20
20
  member { post :cancel }
21
21
  end
22
22
  get "team", to: "team#show"
23
+ get "org", to: "org#show"
23
24
  resources :holidays, only: :index
24
25
  get "calendar", to: "calendar#show"
25
26
  resources :salary_slips, only: %i[index show]
@@ -0,0 +1,8 @@
1
+ class AddManagerToHrLiteEmployeeProfiles < ActiveRecord::Migration[8.0]
2
+ def change
3
+ # Reporting line: the user this employee reports to (L1). The chain
4
+ # upward gives L2, L3, ... and powers the everyone-visible org chart.
5
+ add_column :hr_lite_employee_profiles, :manager_id, :bigint
6
+ add_index :hr_lite_employee_profiles, :manager_id
7
+ end
8
+ end
@@ -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,9 +8,23 @@ 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
 
15
+ attr_reader :leave_year_start_month
16
+
17
+ # Misconfiguration must fail at boot, not as production 500s on every
18
+ # balance screen. Accepts "7" (ENV-friendly) and validates 1..12.
19
+ # Changing this on an install with EXISTING balance rows reinterprets
20
+ # them — set it once at install time (see docs/CONFIGURATION.md).
21
+ def leave_year_start_month=(value)
22
+ month = Integer(value)
23
+ raise ArgumentError, "leave_year_start_month must be 1..12, got #{value.inspect}" unless (1..12).cover?(month)
24
+
25
+ @leave_year_start_month = month
26
+ end
27
+
14
28
  # 0.1.0 pre-release name for public_url_base; kept as an alias so early
15
29
  # adopters' initializers don't break.
16
30
  alias_method :mail_link_base, :public_url_base
@@ -36,11 +50,19 @@ module HrLite
36
50
  emails = HrLite.config.leadership_emails.map { |e| e.to_s.downcase.strip }
37
51
  emails.include?(user.email.to_s.downcase)
38
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
39
60
  @extra_stylesheets = [] # host stylesheets linked AFTER hr_lite.css (CSS-var overrides)
40
61
  @mailer_from = "hr@example.com"
41
62
  @public_url_base = nil # e.g. "https://hr.example.com" — enables email links + HrLite.public_url
42
63
  @notification_matrix = nil # resolved lazily to Notifications::DEFAULT_MATRIX
43
64
  @back_link = nil # optional {label:, url:} for the shell nav
65
+ @leave_year_start_month = 1 # 1 = calendar year; 7 = July–June leave year
44
66
 
45
67
  # Leadership onboarding/offboarding. onboard_user must return a saved
46
68
  # user record (default: create on user_class with whatever of
@@ -0,0 +1,33 @@
1
+ module HrLite
2
+ # The leave year is the 12-month window balances live in. It starts on
3
+ # config.leave_year_start_month (1 = calendar year, 7 = July–June, the
4
+ # Indian travel-industry pattern). A year is identified by the calendar
5
+ # year its FIRST month falls in: with July starts, key 2026 spans
6
+ # 1 Jul 2026 – 30 Jun 2027.
7
+ module LeaveYear
8
+ module_function
9
+
10
+ def start_month
11
+ HrLite.config.leave_year_start_month
12
+ end
13
+
14
+ # The key (integer label) of the leave year containing `date`.
15
+ def key_for(date)
16
+ date.month >= start_month ? date.year : date.year - 1
17
+ end
18
+
19
+ def current_key
20
+ key_for(Date.current)
21
+ end
22
+
23
+ def range(key)
24
+ start = Date.new(key, start_month, 1)
25
+ start..(start >> 12) - 1
26
+ end
27
+
28
+ # "2026" for calendar years, "2026–27" otherwise.
29
+ def label(key)
30
+ start_month == 1 ? key.to_s : format("%d–%02d", key, (key + 1) % 100)
31
+ end
32
+ end
33
+ end
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.3.0"
2
+ VERSION = "0.5.0"
3
3
  end
data/lib/hr_lite.rb CHANGED
@@ -2,6 +2,7 @@ require "hr_lite/version"
2
2
  require "hr_lite/engine"
3
3
  require "hr_lite/configuration"
4
4
  require "hr_lite/current"
5
+ require "hr_lite/leave_year"
5
6
  require "hr_lite/mention_parser"
6
7
  require "hr_lite/notifications"
7
8
  require "hr_lite/seeds"
@@ -37,6 +38,10 @@ module HrLite
37
38
  user.present? && !!config.leadership_check.call(user)
38
39
  end
39
40
 
41
+ def superadmin?(user)
42
+ user.present? && !!config.superadmin_check.call(user)
43
+ end
44
+
40
45
  # Leadership members resolvable to actual user records (for bell
41
46
  # notifications). Emails configured but absent from the user table are
42
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.3.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
@@ -86,6 +87,7 @@ files:
86
87
  - app/controllers/hr_lite/kudos_controller.rb
87
88
  - app/controllers/hr_lite/leave_balances_controller.rb
88
89
  - app/controllers/hr_lite/leave_requests_controller.rb
90
+ - app/controllers/hr_lite/org_controller.rb
89
91
  - app/controllers/hr_lite/regularization_requests_controller.rb
90
92
  - app/controllers/hr_lite/resignations_controller.rb
91
93
  - app/controllers/hr_lite/salary_slips_controller.rb
@@ -197,6 +199,8 @@ files:
197
199
  - app/views/hr_lite/leave_requests/index.html.erb
198
200
  - app/views/hr_lite/leave_requests/new.html.erb
199
201
  - app/views/hr_lite/leave_requests/show.html.erb
202
+ - app/views/hr_lite/org/_node.html.erb
203
+ - app/views/hr_lite/org/show.html.erb
200
204
  - app/views/hr_lite/regularization_requests/index.html.erb
201
205
  - app/views/hr_lite/regularization_requests/new.html.erb
202
206
  - app/views/hr_lite/resignations/show.html.erb
@@ -232,6 +236,8 @@ files:
232
236
  - db/migrate/20260719104315_add_comp_off_to_hr_lite_leave_types.rb
233
237
  - db/migrate/20260719104316_create_hr_lite_comp_off_requests.rb
234
238
  - db/migrate/20260719104317_create_hr_lite_regularization_requests.rb
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
235
241
  - lib/generators/hr_lite/install/install_generator.rb
236
242
  - lib/generators/hr_lite/install/templates/AFTER_INSTALL
237
243
  - lib/generators/hr_lite/install/templates/initializer.rb
@@ -241,6 +247,7 @@ files:
241
247
  - lib/hr_lite/current.rb
242
248
  - lib/hr_lite/engine.rb
243
249
  - lib/hr_lite/geo.rb
250
+ - lib/hr_lite/leave_year.rb
244
251
  - lib/hr_lite/mention_parser.rb
245
252
  - lib/hr_lite/money.rb
246
253
  - lib/hr_lite/notifications.rb