hr_lite 0.13.0 → 0.14.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 +4 -4
- data/CHANGELOG.md +33 -1
- data/app/controllers/hr_lite/admin/assets_controller.rb +53 -0
- data/app/controllers/hr_lite/admin/checklists_controller.rb +32 -0
- data/app/controllers/hr_lite/admin/employees_controller.rb +4 -0
- data/app/controllers/hr_lite/admin/expenses_controller.rb +45 -0
- data/app/controllers/hr_lite/admin/hr_requests_controller.rb +38 -0
- data/app/helpers/hr_lite/application_helper.rb +5 -1
- data/app/models/hr_lite/asset.rb +57 -0
- data/app/models/hr_lite/asset_assignment.rb +24 -0
- data/app/models/hr_lite/checklist_item.rb +43 -0
- data/app/models/hr_lite/checklist_template.rb +50 -0
- data/app/models/hr_lite/employee_profile.rb +13 -0
- data/app/views/hr_lite/admin/assets/index.html.erb +59 -0
- data/app/views/hr_lite/admin/assets/new.html.erb +23 -0
- data/app/views/hr_lite/admin/checklists/index.html.erb +50 -0
- data/app/views/hr_lite/admin/expenses/index.html.erb +54 -0
- data/app/views/hr_lite/admin/hr_requests/index.html.erb +41 -0
- data/app/views/hr_lite/admin/hr_requests/show.html.erb +47 -0
- data/config/routes.rb +12 -0
- data/db/migrate/20260818010401_create_hr_lite_assets_and_onboarding.rb +76 -0
- data/lib/hr_lite/permissions.rb +3 -0
- data/lib/hr_lite/role_seeds.rb +5 -3
- data/lib/hr_lite/version.rb +1 -1
- data/lib/tasks/hr_lite_tasks.rake +2 -1
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '04966d2c7a57a06e49c64c6a375b148c497f83340a79445eb0f243dd783246f0'
|
|
4
|
+
data.tar.gz: 1112874bf22548910a651e53123056fd1341e045598060660c12f93b284789bb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '00219df8c7244175712e7a87768f50301fa2f8b0110ae201f343409064bdb1083cce3eef587ad046cf7e1831a1981eb7d3c6e816e2a9d431cbbb86aaf5ef8c89'
|
|
7
|
+
data.tar.gz: 53af8da6feddf2be67301f9754f2912a08fe246bada0b68eb89b2eb49b70a3279db4ded5645f3438bc859e1a57be3cde29e6c1fc59526db2747d1cc98a5525c6
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.14.0] - 2026-08-18
|
|
11
|
+
|
|
12
|
+
The admin half, plus the two ends of employment that were still manual.
|
|
13
|
+
**Adds one migration.**
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Admin screens for everything shipped since 0.11.0**: decide a claim and
|
|
18
|
+
record the payroll month it was paid with, answer or hand on a help-desk
|
|
19
|
+
request, hand out and take back an asset. The engine had all of this; there
|
|
20
|
+
was no way to do any of it.
|
|
21
|
+
- **Assets.** One live holder at a time, enforced by a partial unique index —
|
|
22
|
+
two open assignments would mean the laptop is in two places, which it is
|
|
23
|
+
not. Assignments are append-only and record what condition it came back in,
|
|
24
|
+
because "screen cracked" is the difference between an asset returned and an
|
|
25
|
+
asset written off.
|
|
26
|
+
- **Joining and leaving checklists**, from templates. The joining list opens
|
|
27
|
+
when a profile is created and the leaving list when an exit date is
|
|
28
|
+
stamped — the checklist nobody remembers to start is the one that does not
|
|
29
|
+
happen. Opening is idempotent, so adding a template later adds only what is
|
|
30
|
+
new. A failure to build one can never stop an employee being created.
|
|
31
|
+
- Each step names the permission its owner should hold, so IT taking back a
|
|
32
|
+
laptop and HR collecting a document are not the same person by accident.
|
|
33
|
+
- Permissions `asset.view`, `asset.manage`, `checklist.manage`.
|
|
34
|
+
|
|
35
|
+
### Why it matters
|
|
36
|
+
|
|
37
|
+
An asset that is never returned is a cost nobody notices, and access that is
|
|
38
|
+
never revoked is a security problem that outlives the employment. Both were
|
|
39
|
+
previously somebody's memory.
|
|
40
|
+
|
|
10
41
|
## [0.13.0] - 2026-08-18
|
|
11
42
|
|
|
12
43
|
The screens. No migration.
|
|
@@ -762,7 +793,8 @@ Initial release.
|
|
|
762
793
|
bus with per-event channel matrix (bell, email, leadership email/bell),
|
|
763
794
|
daily leadership digest and an append-only audit trail.
|
|
764
795
|
|
|
765
|
-
[Unreleased]: https://github.com/kshtzkr/hr_lite/compare/v0.
|
|
796
|
+
[Unreleased]: https://github.com/kshtzkr/hr_lite/compare/v0.14.0...HEAD
|
|
797
|
+
[0.14.0]: https://github.com/kshtzkr/hr_lite/compare/v0.13.0...v0.14.0
|
|
766
798
|
[0.13.0]: https://github.com/kshtzkr/hr_lite/compare/v0.12.0...v0.13.0
|
|
767
799
|
[0.12.0]: https://github.com/kshtzkr/hr_lite/compare/v0.11.0...v0.12.0
|
|
768
800
|
[0.11.0]: https://github.com/kshtzkr/hr_lite/compare/v0.10.0...v0.11.0
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
# Who has the laptop.
|
|
4
|
+
class AssetsController < BaseController
|
|
5
|
+
skip_before_action :require_operations_access!
|
|
6
|
+
before_action :require_assets!
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
@assets = paginate(Asset.order(:status, :name).includes(asset_assignments: :user))
|
|
10
|
+
@outstanding = AssetAssignment.live.includes(:asset, :user)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def new
|
|
14
|
+
@asset = Asset.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create
|
|
18
|
+
@asset = Asset.new(asset_params)
|
|
19
|
+
if @asset.save
|
|
20
|
+
redirect_to admin_assets_path, notice: "#{@asset.name} added."
|
|
21
|
+
else
|
|
22
|
+
render :new, status: :unprocessable_entity
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def assign
|
|
27
|
+
asset = Asset.find(params[:id])
|
|
28
|
+
user = HrLite.user_klass.find(params[:user_id])
|
|
29
|
+
asset.assign_to!(user, actor: hr_current_user)
|
|
30
|
+
redirect_to admin_assets_path, notice: "#{asset.name} given to #{HrLite.display_name(user)}."
|
|
31
|
+
rescue ActiveRecord::RecordInvalid
|
|
32
|
+
redirect_to admin_assets_path, alert: "#{asset.name} is already with somebody."
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def take_back
|
|
36
|
+
asset = Asset.find(params[:id])
|
|
37
|
+
asset.return!(actor: hr_current_user, condition: params[:condition_note],
|
|
38
|
+
status: params[:status].presence_in(Asset::STATUSES) || "available")
|
|
39
|
+
redirect_to admin_assets_path, notice: "#{asset.name} taken back."
|
|
40
|
+
rescue ActiveRecord::RecordInvalid
|
|
41
|
+
redirect_to admin_assets_path, alert: "#{asset.name} is not with anybody."
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def require_assets! = hr_require_permission!("asset.manage", scope: :all)
|
|
47
|
+
|
|
48
|
+
def asset_params
|
|
49
|
+
params.require(:asset).permit(:name, :category, :serial_number, :purchased_on, :notes)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
# Joining and leaving, step by step, with what is overdue at the top.
|
|
4
|
+
class ChecklistsController < BaseController
|
|
5
|
+
skip_before_action :require_operations_access!
|
|
6
|
+
before_action :require_checklists!
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
@kind = params[:kind].presence_in(ChecklistTemplate::KINDS) || "onboarding"
|
|
10
|
+
@items = ChecklistItem.for_kind(@kind).outstanding.includes(:user)
|
|
11
|
+
.order(Arel.sql("due_on IS NULL, due_on"))
|
|
12
|
+
@overdue = @items.select(&:overdue?)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def complete
|
|
16
|
+
item = ChecklistItem.find(params[:id])
|
|
17
|
+
item.complete!(actor: hr_current_user, note: params[:note])
|
|
18
|
+
redirect_to admin_checklists_path(kind: item.kind), notice: "#{item.title} — done."
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def reopen
|
|
22
|
+
item = ChecklistItem.find(params[:id])
|
|
23
|
+
item.reopen!(actor: hr_current_user)
|
|
24
|
+
redirect_to admin_checklists_path(kind: item.kind), notice: "#{item.title} — reopened."
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def require_checklists! = hr_require_permission!("checklist.manage", scope: :all)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -93,6 +93,10 @@ module HrLite
|
|
|
93
93
|
exit_date = params[:date_of_exit].present? ? parse_date_param(params[:date_of_exit]) : Date.current
|
|
94
94
|
|
|
95
95
|
@profile.update!(date_of_exit: exit_date)
|
|
96
|
+
# The last day has a list too — and it is the one that matters most,
|
|
97
|
+
# since an unreturned laptop and un-revoked access both outlive the
|
|
98
|
+
# employment.
|
|
99
|
+
ChecklistItem.open_for!(@profile.user, kind: "offboarding", anchor_date: exit_date)
|
|
96
100
|
begin
|
|
97
101
|
HrLite.config.offboard_user.call(@profile.user)
|
|
98
102
|
rescue => e
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
# The other side of the claim: decide it, then say when it was paid.
|
|
4
|
+
class ExpensesController < BaseController
|
|
5
|
+
skip_before_action :require_operations_access!
|
|
6
|
+
before_action :require_deciding!
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
@status = params[:status].presence_in(Expense::STATUSES) || "submitted"
|
|
10
|
+
scope = Expense.where(status: @status).includes(:user, :category).recent_first
|
|
11
|
+
@expenses = paginate(hr_scope(scope, "expense.approve"))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def approve
|
|
15
|
+
expense = decidable.find(params[:id])
|
|
16
|
+
expense.approve!(actor: hr_current_user, note: params[:decision_note].presence)
|
|
17
|
+
redirect_to admin_expenses_path, notice: "Claim approved."
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reject
|
|
21
|
+
expense = decidable.find(params[:id])
|
|
22
|
+
note = params[:decision_note].to_s.strip
|
|
23
|
+
return redirect_to admin_expenses_path, alert: "A note is required to reject." if note.blank?
|
|
24
|
+
|
|
25
|
+
expense.reject!(actor: hr_current_user, note: note)
|
|
26
|
+
redirect_to admin_expenses_path, notice: "Claim rejected."
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reimburse
|
|
30
|
+
expense = decidable.find(params[:id])
|
|
31
|
+
month = parse_month_param(params[:period_month])
|
|
32
|
+
expense.reimburse!(actor: hr_current_user, period_month: month)
|
|
33
|
+
redirect_to admin_expenses_path, notice: "Marked reimbursed with #{month.strftime('%B %Y')}."
|
|
34
|
+
rescue ActiveRecord::RecordInvalid
|
|
35
|
+
redirect_to admin_expenses_path, alert: "Only an approved claim can be reimbursed."
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def require_deciding! = hr_require_permission!("expense.approve", scope: :team)
|
|
41
|
+
|
|
42
|
+
def decidable = hr_scope(Expense.all, "expense.approve")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
# The desk itself.
|
|
4
|
+
class HrRequestsController < BaseController
|
|
5
|
+
skip_before_action :require_operations_access!
|
|
6
|
+
before_action :require_desk!
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
@status = params[:status].presence_in(HrRequest::STATUSES) || "open"
|
|
10
|
+
@requests = paginate(HrRequest.where(status: @status).includes(:user, :assigned_to)
|
|
11
|
+
.recent_first)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def show
|
|
15
|
+
@request = HrRequest.find(params[:id])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def assign
|
|
19
|
+
request = HrRequest.find(params[:id])
|
|
20
|
+
assignee = HrLite.user_klass.find(params[:assignee_id])
|
|
21
|
+
request.assign!(actor: hr_current_user, assignee: assignee)
|
|
22
|
+
redirect_to admin_hr_request_path(request), notice: "Assigned to #{HrLite.display_name(assignee)}."
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def resolve
|
|
26
|
+
request = HrRequest.find(params[:id])
|
|
27
|
+
request.resolve!(actor: hr_current_user, resolution: params[:resolution].to_s.strip)
|
|
28
|
+
redirect_to admin_hr_requests_path, notice: "Answered."
|
|
29
|
+
rescue ArgumentError
|
|
30
|
+
redirect_to admin_hr_request_path(request), alert: "Write an answer before resolving."
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def require_desk! = hr_require_permission!("hr_request.manage", scope: :all)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -26,7 +26,11 @@ module HrLite
|
|
|
26
26
|
{ label: "Overview", path: :admin_overview_path, match: [ "/admin/overview" ] },
|
|
27
27
|
{ label: "Team attendance", path: :admin_attendances_path, match: [ "/admin/attendances" ] },
|
|
28
28
|
{ label: "Approvals", path: :admin_leave_requests_path, match: [ "/admin/leave_requests", "/admin/leave_balances", "/admin/comp_off_requests", "/admin/regularization_requests" ] },
|
|
29
|
-
{ label: "Reports", path: :admin_reports_path, match: [ "/admin/reports" ] }
|
|
29
|
+
{ label: "Reports", path: :admin_reports_path, match: [ "/admin/reports" ] },
|
|
30
|
+
{ label: "Claims", path: :admin_expenses_path, match: [ "/admin/expenses" ] },
|
|
31
|
+
{ label: "Help desk", path: :admin_hr_requests_path, match: [ "/admin/hr_requests" ] },
|
|
32
|
+
{ label: "Assets", path: :admin_assets_path, match: [ "/admin/assets" ] },
|
|
33
|
+
{ label: "Joining & exits", path: :admin_checklists_path, match: [ "/admin/checklists" ] }
|
|
30
34
|
].freeze
|
|
31
35
|
|
|
32
36
|
LEADERSHIP_NAV_ITEMS = [
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# A laptop, a phone, a SIM. An asset nobody tracks is a cost nobody
|
|
3
|
+
# notices, and the moment it matters is somebody's last day.
|
|
4
|
+
class Asset < ApplicationRecord
|
|
5
|
+
include Audited
|
|
6
|
+
|
|
7
|
+
STATUSES = %w[available assigned returned lost damaged retired].freeze
|
|
8
|
+
CATEGORIES = %w[laptop phone sim id_card accessory vehicle other].freeze
|
|
9
|
+
|
|
10
|
+
has_many :asset_assignments, class_name: "HrLite::AssetAssignment", dependent: :destroy
|
|
11
|
+
|
|
12
|
+
validates :name, presence: true
|
|
13
|
+
validates :status, inclusion: { in: STATUSES }
|
|
14
|
+
validates :category, inclusion: { in: CATEGORIES }
|
|
15
|
+
validates :serial_number, uniqueness: { allow_blank: true }
|
|
16
|
+
|
|
17
|
+
scope :available, -> { where(status: "available") }
|
|
18
|
+
scope :out, -> { where(status: "assigned") }
|
|
19
|
+
|
|
20
|
+
STATUSES.each { |s| define_method("#{s}?") { status == s } }
|
|
21
|
+
|
|
22
|
+
def live_assignment = asset_assignments.find_by(returned_on: nil)
|
|
23
|
+
|
|
24
|
+
def holder = live_assignment&.user
|
|
25
|
+
|
|
26
|
+
def assign_to!(user, actor: nil, on: Date.current)
|
|
27
|
+
raise ActiveRecord::RecordInvalid.new(self), "already assigned" if live_assignment
|
|
28
|
+
|
|
29
|
+
transaction do
|
|
30
|
+
asset_assignments.create!(user_id: user.id, assigned_on: on, assigned_by_id: actor&.id)
|
|
31
|
+
update!(status: "assigned")
|
|
32
|
+
end
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# `condition` records what came back — "screen cracked" is the difference
|
|
37
|
+
# between an asset returned and an asset written off.
|
|
38
|
+
def return!(actor: nil, on: Date.current, condition: nil, status: "available")
|
|
39
|
+
assignment = live_assignment
|
|
40
|
+
raise ActiveRecord::RecordInvalid.new(self), "not assigned" if assignment.nil?
|
|
41
|
+
|
|
42
|
+
transaction do
|
|
43
|
+
assignment.update!(returned_on: on, condition_note: condition.presence)
|
|
44
|
+
update!(status: status)
|
|
45
|
+
end
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Everything still out with somebody who has left — the report that
|
|
50
|
+
# should exist before an exit, not after.
|
|
51
|
+
def self.outstanding_for(user)
|
|
52
|
+
joins(:asset_assignments)
|
|
53
|
+
.where(hr_lite_asset_assignments: { user_id: user.id, returned_on: nil })
|
|
54
|
+
.distinct
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Who had it, when, and what state it came back in. Append-only: editing an
|
|
3
|
+
# assignment away is how a missing laptop stops being anybody's.
|
|
4
|
+
class AssetAssignment < ApplicationRecord
|
|
5
|
+
belongs_to :asset, class_name: "HrLite::Asset"
|
|
6
|
+
belongs_to :user, class_name: HrLite.config.user_class
|
|
7
|
+
belongs_to :assigned_by, class_name: HrLite.config.user_class, optional: true
|
|
8
|
+
|
|
9
|
+
validates :assigned_on, presence: true
|
|
10
|
+
validate :returned_after_it_was_given
|
|
11
|
+
|
|
12
|
+
scope :live, -> { where(returned_on: nil) }
|
|
13
|
+
|
|
14
|
+
def live? = returned_on.nil?
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def returned_after_it_was_given
|
|
19
|
+
return if returned_on.nil? || assigned_on.nil? || returned_on >= assigned_on
|
|
20
|
+
|
|
21
|
+
errors.add(:returned_on, "cannot be before the day it was handed over")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# One step for one person. Kept even when its template is later deleted —
|
|
3
|
+
# what a company DID on somebody's first day is history, and history should
|
|
4
|
+
# not change because a template was tidied up.
|
|
5
|
+
class ChecklistItem < ApplicationRecord
|
|
6
|
+
include Audited
|
|
7
|
+
|
|
8
|
+
belongs_to :template, class_name: "HrLite::ChecklistTemplate", optional: true
|
|
9
|
+
belongs_to :user, class_name: HrLite.config.user_class
|
|
10
|
+
belongs_to :completed_by, class_name: HrLite.config.user_class, optional: true
|
|
11
|
+
|
|
12
|
+
validates :kind, inclusion: { in: ChecklistTemplate::KINDS }
|
|
13
|
+
validates :title, presence: true
|
|
14
|
+
|
|
15
|
+
scope :outstanding, -> { where(completed_at: nil) }
|
|
16
|
+
scope :for_kind, ->(kind) { where(kind: kind.to_s) }
|
|
17
|
+
scope :overdue, ->(on = Date.current) { outstanding.where(due_on: ...on) }
|
|
18
|
+
|
|
19
|
+
def done? = completed_at.present?
|
|
20
|
+
|
|
21
|
+
def overdue?(on = Date.current) = !done? && due_on.present? && due_on < on
|
|
22
|
+
|
|
23
|
+
def complete!(actor:, note: nil)
|
|
24
|
+
update!(completed_at: Time.current, completed_by_id: actor&.id, note: note.presence)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reopen!(actor:)
|
|
28
|
+
update!(completed_at: nil, completed_by_id: nil)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Builds somebody's list from the active templates. Idempotent: running
|
|
32
|
+
# it twice does not double the list, so a re-run after adding a template
|
|
33
|
+
# adds only what is new.
|
|
34
|
+
def self.open_for!(user, kind:, anchor_date:)
|
|
35
|
+
ChecklistTemplate.for_kind(kind).filter_map do |template|
|
|
36
|
+
next if exists?(user_id: user.id, kind: kind.to_s, title: template.title)
|
|
37
|
+
|
|
38
|
+
create!(user_id: user.id, template: template, kind: kind.to_s, title: template.title,
|
|
39
|
+
due_on: anchor_date + template.due_offset_days)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# "Collect the signed offer letter", "Revoke email access". The steps a
|
|
3
|
+
# company means to take every time and forgets once.
|
|
4
|
+
class ChecklistTemplate < ApplicationRecord
|
|
5
|
+
include Audited
|
|
6
|
+
|
|
7
|
+
KINDS = %w[onboarding offboarding].freeze
|
|
8
|
+
|
|
9
|
+
validates :kind, inclusion: { in: KINDS }
|
|
10
|
+
validates :title, presence: true
|
|
11
|
+
validate :owner_permission_is_declared
|
|
12
|
+
|
|
13
|
+
scope :active, -> { where(active: true) }
|
|
14
|
+
scope :for_kind, ->(kind) { active.where(kind: kind.to_s).order(:position, :id) }
|
|
15
|
+
|
|
16
|
+
def self.seed_defaults!
|
|
17
|
+
[
|
|
18
|
+
{ kind: "onboarding", title: "Collect signed offer letter", position: 10,
|
|
19
|
+
owner_permission: "document.manage" },
|
|
20
|
+
{ kind: "onboarding", title: "Collect PAN and Aadhaar", position: 20,
|
|
21
|
+
owner_permission: "document.manage" },
|
|
22
|
+
{ kind: "onboarding", title: "Create payroll record and salary structure",
|
|
23
|
+
position: 30, owner_permission: "salary.manage", due_offset_days: 3 },
|
|
24
|
+
{ kind: "onboarding", title: "Hand over laptop and access", position: 40,
|
|
25
|
+
owner_permission: "profile.manage" },
|
|
26
|
+
{ kind: "offboarding", title: "Collect laptop and accessories", position: 10,
|
|
27
|
+
owner_permission: "profile.manage" },
|
|
28
|
+
{ kind: "offboarding", title: "Revoke email and system access", position: 20,
|
|
29
|
+
owner_permission: "profile.manage" },
|
|
30
|
+
{ kind: "offboarding", title: "Full and final settlement", position: 30,
|
|
31
|
+
owner_permission: "payroll.manage", due_offset_days: 30 },
|
|
32
|
+
{ kind: "offboarding", title: "Issue relieving and experience letters",
|
|
33
|
+
position: 40, owner_permission: "document.manage", due_offset_days: 7 }
|
|
34
|
+
].filter_map do |attributes|
|
|
35
|
+
next if exists?(kind: attributes[:kind], title: attributes[:title])
|
|
36
|
+
|
|
37
|
+
create!(attributes)
|
|
38
|
+
"#{attributes[:kind]}: #{attributes[:title]}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def owner_permission_is_declared
|
|
45
|
+
return if owner_permission.blank? || Permissions.valid?(owner_permission)
|
|
46
|
+
|
|
47
|
+
errors.add(:owner_permission, "is not a known permission")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -6,6 +6,7 @@ module HrLite
|
|
|
6
6
|
class EmployeeProfile < ApplicationRecord
|
|
7
7
|
include EncryptedMoney
|
|
8
8
|
include Audited
|
|
9
|
+
after_create_commit :open_onboarding_checklist
|
|
9
10
|
|
|
10
11
|
belongs_to :user, class_name: HrLite.config.user_class
|
|
11
12
|
# Reporting line (L1). Optional; the org chart treats manager-less
|
|
@@ -152,5 +153,17 @@ module HrLite
|
|
|
152
153
|
|
|
153
154
|
errors.add(:date_of_exit, "must be after joining")
|
|
154
155
|
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
# Day one has a list, and it is the same list every time. Opened on
|
|
160
|
+
# create rather than by hand, because the one nobody remembers to start
|
|
161
|
+
# is the one that does not happen.
|
|
162
|
+
def open_onboarding_checklist
|
|
163
|
+
ChecklistItem.open_for!(user, kind: "onboarding", anchor_date: date_of_joining)
|
|
164
|
+
rescue => e
|
|
165
|
+
# A checklist must never be the reason an employee cannot be created.
|
|
166
|
+
Rails.logger.error("[hr_lite] onboarding checklist failed: #{e.class}: #{e.message}")
|
|
167
|
+
end
|
|
155
168
|
end
|
|
156
169
|
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Assets" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Assets</h1>
|
|
4
|
+
<div class="hrl-page__actions">
|
|
5
|
+
<a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_admin_asset_path %>">Add asset</a>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<% if @outstanding.any? %>
|
|
10
|
+
<section class="hrl-card">
|
|
11
|
+
<h2 class="hrl-card__title">Currently out</h2>
|
|
12
|
+
<ul>
|
|
13
|
+
<% @outstanding.each do |assignment| %>
|
|
14
|
+
<li class="hrl-small">
|
|
15
|
+
<strong><%= assignment.asset.name %></strong> —
|
|
16
|
+
<%= hr_display_name(assignment.user) %>,
|
|
17
|
+
since <%= assignment.assigned_on.strftime("%d %b %Y") %>
|
|
18
|
+
</li>
|
|
19
|
+
<% end %>
|
|
20
|
+
</ul>
|
|
21
|
+
</section>
|
|
22
|
+
<% end %>
|
|
23
|
+
|
|
24
|
+
<section class="hrl-card">
|
|
25
|
+
<div class="hrl-table-wrap">
|
|
26
|
+
<table class="hrl-table hrl-table--stack">
|
|
27
|
+
<thead><tr><th>Asset</th><th>Status</th><th>With</th><th></th></tr></thead>
|
|
28
|
+
<tbody>
|
|
29
|
+
<% @assets.each do |asset| %>
|
|
30
|
+
<tr>
|
|
31
|
+
<td data-label="Asset">
|
|
32
|
+
<strong><%= asset.name %></strong>
|
|
33
|
+
<div class="hrl-small hrl-muted">
|
|
34
|
+
<%= asset.category.humanize %><%= " · #{asset.serial_number}" if asset.serial_number.present? %>
|
|
35
|
+
</div>
|
|
36
|
+
</td>
|
|
37
|
+
<td data-label="Status"><%= hrl_request_status_badge(asset.status) %></td>
|
|
38
|
+
<td data-label="With"><%= asset.holder ? hr_display_name(asset.holder) : "—" %></td>
|
|
39
|
+
<td data-label="">
|
|
40
|
+
<% if asset.assigned? %>
|
|
41
|
+
<%= form_with url: hr_lite.take_back_admin_asset_path(asset), method: :post do %>
|
|
42
|
+
<%= text_field_tag :condition_note, nil, placeholder: "Condition" %>
|
|
43
|
+
<%= submit_tag "Take back", class: "hrl-btn" %>
|
|
44
|
+
<% end %>
|
|
45
|
+
<% elsif asset.available? %>
|
|
46
|
+
<%= form_with url: hr_lite.assign_admin_asset_path(asset), method: :post do %>
|
|
47
|
+
<%= select_tag :user_id,
|
|
48
|
+
options_for_select(HrLite.employees.map { |u| [ hr_display_name(u), u.id ] }) %>
|
|
49
|
+
<%= submit_tag "Hand over", class: "hrl-btn" %>
|
|
50
|
+
<% end %>
|
|
51
|
+
<% end %>
|
|
52
|
+
</td>
|
|
53
|
+
</tr>
|
|
54
|
+
<% end %>
|
|
55
|
+
</tbody>
|
|
56
|
+
</table>
|
|
57
|
+
</div>
|
|
58
|
+
<%= hrl_pagination %>
|
|
59
|
+
</section>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Add asset" } %>
|
|
2
|
+
<div class="hrl-page__head"><h1 class="hrl-page__title">Add asset</h1></div>
|
|
3
|
+
|
|
4
|
+
<section class="hrl-card">
|
|
5
|
+
<%= form_with model: @asset, url: hr_lite.admin_assets_path, method: :post, local: true do |f| %>
|
|
6
|
+
<%= render "hr_lite/shared/form_errors", record: @asset %>
|
|
7
|
+
<div class="hrl-field"><%= f.label :name %><%= f.text_field :name %></div>
|
|
8
|
+
<div class="hrl-field">
|
|
9
|
+
<%= f.label :category %>
|
|
10
|
+
<%= f.select :category, HrLite::Asset::CATEGORIES.map { |c| [ c.humanize, c ] } %>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="hrl-field">
|
|
13
|
+
<%= f.label :serial_number, "Serial number (optional)" %>
|
|
14
|
+
<%= f.text_field :serial_number %>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="hrl-field"><%= f.label :purchased_on %><%= f.date_field :purchased_on %></div>
|
|
17
|
+
<div class="hrl-field"><%= f.label :notes %><%= f.text_field :notes %></div>
|
|
18
|
+
<div class="hrl-form-actions">
|
|
19
|
+
<%= f.submit "Save", class: "hrl-btn hrl-btn--primary" %>
|
|
20
|
+
<a class="hrl-btn" href="<%= hr_lite.admin_assets_path %>">Cancel</a>
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
</section>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Joining & exits" } %>
|
|
2
|
+
<div class="hrl-page__head"><h1 class="hrl-page__title">Joining & exits</h1></div>
|
|
3
|
+
|
|
4
|
+
<section class="hrl-card">
|
|
5
|
+
<div class="hrl-row">
|
|
6
|
+
<% HrLite::ChecklistTemplate::KINDS.each do |kind| %>
|
|
7
|
+
<a class="hrl-btn <%= "hrl-btn--primary" if kind == @kind %>"
|
|
8
|
+
href="<%= hr_lite.admin_checklists_path(kind: kind) %>"><%= kind.humanize %></a>
|
|
9
|
+
<% end %>
|
|
10
|
+
</div>
|
|
11
|
+
</section>
|
|
12
|
+
|
|
13
|
+
<% if @overdue.any? %>
|
|
14
|
+
<section class="hrl-card">
|
|
15
|
+
<h2 class="hrl-card__title"><%= @overdue.size %> overdue</h2>
|
|
16
|
+
<p class="hrl-small hrl-muted">
|
|
17
|
+
An unreturned laptop and un-revoked access both outlive the employment.
|
|
18
|
+
</p>
|
|
19
|
+
</section>
|
|
20
|
+
<% end %>
|
|
21
|
+
|
|
22
|
+
<section class="hrl-card">
|
|
23
|
+
<% if @items.empty? %>
|
|
24
|
+
<p class="hrl-muted">Nothing outstanding.</p>
|
|
25
|
+
<% else %>
|
|
26
|
+
<div class="hrl-table-wrap">
|
|
27
|
+
<table class="hrl-table hrl-table--stack">
|
|
28
|
+
<thead><tr><th>Step</th><th>Who</th><th>Due</th><th></th></tr></thead>
|
|
29
|
+
<tbody>
|
|
30
|
+
<% @items.each do |item| %>
|
|
31
|
+
<tr>
|
|
32
|
+
<td data-label="Step">
|
|
33
|
+
<%= item.title %>
|
|
34
|
+
<% if item.overdue? %><span class="hrl-badge hrl-badge--bad">Overdue</span><% end %>
|
|
35
|
+
</td>
|
|
36
|
+
<td data-label="Who"><%= hr_display_name(item.user) %></td>
|
|
37
|
+
<td data-label="Due"><%= item.due_on&.strftime("%d %b %Y") || "—" %></td>
|
|
38
|
+
<td data-label="">
|
|
39
|
+
<%= form_with url: hr_lite.complete_admin_checklist_path(item), method: :post do %>
|
|
40
|
+
<%= text_field_tag :note, nil, placeholder: "Note" %>
|
|
41
|
+
<%= submit_tag "Done", class: "hrl-btn hrl-btn--primary" %>
|
|
42
|
+
<% end %>
|
|
43
|
+
</td>
|
|
44
|
+
</tr>
|
|
45
|
+
<% end %>
|
|
46
|
+
</tbody>
|
|
47
|
+
</table>
|
|
48
|
+
</div>
|
|
49
|
+
<% end %>
|
|
50
|
+
</section>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Claims" } %>
|
|
2
|
+
<div class="hrl-page__head"><h1 class="hrl-page__title">Expense claims</h1></div>
|
|
3
|
+
|
|
4
|
+
<section class="hrl-card">
|
|
5
|
+
<div class="hrl-row">
|
|
6
|
+
<% HrLite::Expense::STATUSES.each do |status| %>
|
|
7
|
+
<a class="hrl-btn <%= "hrl-btn--primary" if status == @status %>"
|
|
8
|
+
href="<%= hr_lite.admin_expenses_path(status: status) %>"><%= status.humanize %></a>
|
|
9
|
+
<% end %>
|
|
10
|
+
</div>
|
|
11
|
+
</section>
|
|
12
|
+
|
|
13
|
+
<section class="hrl-card">
|
|
14
|
+
<% if @expenses.empty? %>
|
|
15
|
+
<p class="hrl-muted">Nothing <%= @status %>.</p>
|
|
16
|
+
<% else %>
|
|
17
|
+
<div class="hrl-table-wrap">
|
|
18
|
+
<table class="hrl-table hrl-table--stack">
|
|
19
|
+
<thead><tr><th>Who</th><th>What</th><th>Amount</th><th></th></tr></thead>
|
|
20
|
+
<tbody>
|
|
21
|
+
<% @expenses.each do |expense| %>
|
|
22
|
+
<tr>
|
|
23
|
+
<td data-label="Who"><%= hr_display_name(expense.user) %></td>
|
|
24
|
+
<td data-label="What">
|
|
25
|
+
<%= expense.category.name %>
|
|
26
|
+
<div class="hrl-small hrl-muted">
|
|
27
|
+
<%= expense.description %> · <%= expense.spent_on.strftime("%d %b %Y") %>
|
|
28
|
+
</div>
|
|
29
|
+
</td>
|
|
30
|
+
<td data-label="Amount" class="hrl-num"><%= hrl_money(expense.amount) %></td>
|
|
31
|
+
<td data-label="">
|
|
32
|
+
<% if expense.submitted? %>
|
|
33
|
+
<%= form_with url: hr_lite.approve_admin_expense_path(expense), method: :post do %>
|
|
34
|
+
<%= submit_tag "Approve", class: "hrl-btn hrl-btn--primary" %>
|
|
35
|
+
<% end %>
|
|
36
|
+
<%= form_with url: hr_lite.reject_admin_expense_path(expense), method: :post do %>
|
|
37
|
+
<%= text_field_tag :decision_note, nil, placeholder: "Why not" %>
|
|
38
|
+
<%= submit_tag "Reject", class: "hrl-btn" %>
|
|
39
|
+
<% end %>
|
|
40
|
+
<% elsif expense.approved? %>
|
|
41
|
+
<%= form_with url: hr_lite.reimburse_admin_expense_path(expense), method: :post do %>
|
|
42
|
+
<%= text_field_tag :period_month, Date.current.strftime("%Y-%m"), size: 8 %>
|
|
43
|
+
<%= submit_tag "Mark reimbursed", class: "hrl-btn" %>
|
|
44
|
+
<% end %>
|
|
45
|
+
<% end %>
|
|
46
|
+
</td>
|
|
47
|
+
</tr>
|
|
48
|
+
<% end %>
|
|
49
|
+
</tbody>
|
|
50
|
+
</table>
|
|
51
|
+
</div>
|
|
52
|
+
<%= hrl_pagination %>
|
|
53
|
+
<% end %>
|
|
54
|
+
</section>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Help desk" } %>
|
|
2
|
+
<div class="hrl-page__head"><h1 class="hrl-page__title">Help desk</h1></div>
|
|
3
|
+
|
|
4
|
+
<section class="hrl-card">
|
|
5
|
+
<div class="hrl-row">
|
|
6
|
+
<% HrLite::HrRequest::STATUSES.each do |status| %>
|
|
7
|
+
<a class="hrl-btn <%= "hrl-btn--primary" if status == @status %>"
|
|
8
|
+
href="<%= hr_lite.admin_hr_requests_path(status: status) %>"><%= status.humanize %></a>
|
|
9
|
+
<% end %>
|
|
10
|
+
</div>
|
|
11
|
+
</section>
|
|
12
|
+
|
|
13
|
+
<section class="hrl-card">
|
|
14
|
+
<% if @requests.empty? %>
|
|
15
|
+
<p class="hrl-muted">Nothing <%= @status.humanize.downcase %>.</p>
|
|
16
|
+
<% else %>
|
|
17
|
+
<div class="hrl-table-wrap">
|
|
18
|
+
<table class="hrl-table hrl-table--stack">
|
|
19
|
+
<thead><tr><th>Request</th><th>Who</th><th>With</th><th></th></tr></thead>
|
|
20
|
+
<tbody>
|
|
21
|
+
<% @requests.each do |request| %>
|
|
22
|
+
<tr>
|
|
23
|
+
<td data-label="Request">
|
|
24
|
+
<a href="<%= hr_lite.admin_hr_request_path(request) %>"><%= request.subject %></a>
|
|
25
|
+
<div class="hrl-small hrl-muted"><%= request.category_label %></div>
|
|
26
|
+
</td>
|
|
27
|
+
<td data-label="Who"><%= hr_display_name(request.user) %></td>
|
|
28
|
+
<td data-label="With">
|
|
29
|
+
<%= request.assigned_to ? hr_display_name(request.assigned_to) : "—" %>
|
|
30
|
+
</td>
|
|
31
|
+
<td data-label="">
|
|
32
|
+
<a class="hrl-small" href="<%= hr_lite.admin_hr_request_path(request) %>">Open</a>
|
|
33
|
+
</td>
|
|
34
|
+
</tr>
|
|
35
|
+
<% end %>
|
|
36
|
+
</tbody>
|
|
37
|
+
</table>
|
|
38
|
+
</div>
|
|
39
|
+
<%= hrl_pagination %>
|
|
40
|
+
<% end %>
|
|
41
|
+
</section>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<% content_for(:page_title) { @request.subject } %>
|
|
2
|
+
<div class="hrl-page__head"><h1 class="hrl-page__title"><%= @request.subject %></h1></div>
|
|
3
|
+
|
|
4
|
+
<section class="hrl-card">
|
|
5
|
+
<p class="hrl-small hrl-muted">
|
|
6
|
+
<%= @request.category_label %> · <%= hr_display_name(@request.user) %> ·
|
|
7
|
+
<%= @request.created_at.to_date.strftime("%d %b %Y") %> ·
|
|
8
|
+
<%= hrl_request_status_badge(@request.status) %>
|
|
9
|
+
</p>
|
|
10
|
+
<% if @request.body.present? %>
|
|
11
|
+
<div class="hrl-prose"><%= simple_format(@request.body) %></div>
|
|
12
|
+
<% end %>
|
|
13
|
+
</section>
|
|
14
|
+
|
|
15
|
+
<% unless @request.resolved? || @request.closed? || @request.cancelled? %>
|
|
16
|
+
<section class="hrl-card">
|
|
17
|
+
<h2 class="hrl-card__title">Answer</h2>
|
|
18
|
+
<%= form_with url: hr_lite.resolve_admin_hr_request_path(@request), method: :post do %>
|
|
19
|
+
<div class="hrl-field">
|
|
20
|
+
<%= label_tag :resolution, "What you did" %>
|
|
21
|
+
<%= text_area_tag :resolution, nil, rows: 4 %>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="hrl-form-actions">
|
|
24
|
+
<%= submit_tag "Send answer", class: "hrl-btn hrl-btn--primary" %>
|
|
25
|
+
</div>
|
|
26
|
+
<% end %>
|
|
27
|
+
|
|
28
|
+
<%= form_with url: hr_lite.assign_admin_hr_request_path(@request), method: :post do %>
|
|
29
|
+
<div class="hrl-field">
|
|
30
|
+
<%= label_tag :assignee_id, "Or hand it to somebody" %>
|
|
31
|
+
<%= select_tag :assignee_id,
|
|
32
|
+
options_for_select(HrLite.users_holding("hr_request.manage")
|
|
33
|
+
.map { |u| [ hr_display_name(u), u.id ] }) %>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="hrl-form-actions">
|
|
36
|
+
<%= submit_tag "Assign", class: "hrl-btn" %>
|
|
37
|
+
</div>
|
|
38
|
+
<% end %>
|
|
39
|
+
</section>
|
|
40
|
+
<% end %>
|
|
41
|
+
|
|
42
|
+
<% if @request.resolution.present? %>
|
|
43
|
+
<section class="hrl-card">
|
|
44
|
+
<h2 class="hrl-card__title">Answered</h2>
|
|
45
|
+
<div class="hrl-prose"><%= simple_format(@request.resolution) %></div>
|
|
46
|
+
</section>
|
|
47
|
+
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -62,6 +62,18 @@ HrLite::Engine.routes.draw do
|
|
|
62
62
|
end
|
|
63
63
|
resources :statutory_rate_cards, only: %i[index new create edit update]
|
|
64
64
|
resources :reports, only: %i[index show]
|
|
65
|
+
resources :expenses, only: :index do
|
|
66
|
+
member { post :approve; post :reject; post :reimburse }
|
|
67
|
+
end
|
|
68
|
+
resources :hr_requests, only: %i[index show] do
|
|
69
|
+
member { post :assign; post :resolve }
|
|
70
|
+
end
|
|
71
|
+
resources :assets, only: %i[index new create] do
|
|
72
|
+
member { post :assign; post :take_back }
|
|
73
|
+
end
|
|
74
|
+
resources :checklists, only: :index do
|
|
75
|
+
member { post :complete; post :reopen }
|
|
76
|
+
end
|
|
65
77
|
resources :leave_types, except: :show
|
|
66
78
|
resources :office_locations, except: :show
|
|
67
79
|
resources :holidays, only: %i[index create update destroy] do
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
class CreateHrLiteAssetsAndOnboarding < ActiveRecord::Migration[8.1]
|
|
2
|
+
# The two ends of employment that were still manual: the laptop somebody is
|
|
3
|
+
# given on day one, and the checklist nobody remembers on their last.
|
|
4
|
+
#
|
|
5
|
+
# An asset that is never returned is a cost nobody notices, and access that
|
|
6
|
+
# is never revoked is a security problem that outlives the employment.
|
|
7
|
+
def change
|
|
8
|
+
create_table :hr_lite_assets do |t|
|
|
9
|
+
t.string :name, null: false
|
|
10
|
+
# laptop | phone | sim | id_card | accessory | vehicle | other
|
|
11
|
+
t.string :category, null: false, default: "other"
|
|
12
|
+
t.string :serial_number
|
|
13
|
+
t.date :purchased_on
|
|
14
|
+
# available | assigned | returned | lost | damaged | retired
|
|
15
|
+
t.string :status, null: false, default: "available"
|
|
16
|
+
t.text :notes
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
add_index :hr_lite_assets, :status
|
|
20
|
+
add_index :hr_lite_assets, :serial_number, unique: true,
|
|
21
|
+
where: "serial_number IS NOT NULL"
|
|
22
|
+
add_check_constraint :hr_lite_assets,
|
|
23
|
+
"status IN ('available', 'assigned', 'returned', 'lost', 'damaged', 'retired')",
|
|
24
|
+
name: "hr_lite_assets_status_check"
|
|
25
|
+
|
|
26
|
+
# Append-only: who had it, when, and what state it came back in. Editing
|
|
27
|
+
# an assignment away is how a missing laptop stops being anybody's.
|
|
28
|
+
create_table :hr_lite_asset_assignments do |t|
|
|
29
|
+
t.references :asset, null: false, index: true,
|
|
30
|
+
foreign_key: { to_table: :hr_lite_assets, on_delete: :cascade }
|
|
31
|
+
t.bigint :user_id, null: false
|
|
32
|
+
t.date :assigned_on, null: false
|
|
33
|
+
t.date :returned_on
|
|
34
|
+
t.string :condition_note
|
|
35
|
+
t.bigint :assigned_by_id
|
|
36
|
+
t.timestamps
|
|
37
|
+
end
|
|
38
|
+
add_index :hr_lite_asset_assignments, %i[user_id returned_on]
|
|
39
|
+
# One live holder at a time — two open assignments means the asset is in
|
|
40
|
+
# two places, which it is not.
|
|
41
|
+
add_index :hr_lite_asset_assignments, :asset_id,
|
|
42
|
+
unique: true, where: "returned_on IS NULL",
|
|
43
|
+
name: "index_hr_lite_asset_assignments_one_live_per_asset"
|
|
44
|
+
|
|
45
|
+
create_table :hr_lite_checklist_templates do |t|
|
|
46
|
+
# onboarding | offboarding
|
|
47
|
+
t.string :kind, null: false
|
|
48
|
+
t.string :title, null: false
|
|
49
|
+
t.integer :position, null: false, default: 0
|
|
50
|
+
# Which permission the person who ticks it should hold — IT hands back
|
|
51
|
+
# a laptop, HR collects a document, and neither should be the other.
|
|
52
|
+
t.string :owner_permission
|
|
53
|
+
# Days from the joining or leaving date this is due.
|
|
54
|
+
t.integer :due_offset_days, null: false, default: 0
|
|
55
|
+
t.boolean :active, null: false, default: true
|
|
56
|
+
t.timestamps
|
|
57
|
+
end
|
|
58
|
+
add_index :hr_lite_checklist_templates, %i[kind position]
|
|
59
|
+
add_check_constraint :hr_lite_checklist_templates, "kind IN ('onboarding', 'offboarding')",
|
|
60
|
+
name: "hr_lite_checklist_templates_kind_check"
|
|
61
|
+
|
|
62
|
+
create_table :hr_lite_checklist_items do |t|
|
|
63
|
+
t.bigint :user_id, null: false
|
|
64
|
+
t.references :template, index: true,
|
|
65
|
+
foreign_key: { to_table: :hr_lite_checklist_templates, on_delete: :nullify }
|
|
66
|
+
t.string :kind, null: false
|
|
67
|
+
t.string :title, null: false
|
|
68
|
+
t.date :due_on
|
|
69
|
+
t.datetime :completed_at
|
|
70
|
+
t.bigint :completed_by_id
|
|
71
|
+
t.string :note
|
|
72
|
+
t.timestamps
|
|
73
|
+
end
|
|
74
|
+
add_index :hr_lite_checklist_items, %i[user_id kind completed_at]
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/hr_lite/permissions.rb
CHANGED
|
@@ -56,6 +56,9 @@ module HrLite
|
|
|
56
56
|
"hr_request.manage" => [ "Help desk", "Answer and assign HR requests" ],
|
|
57
57
|
"policy.view" => [ "Policies", "Read published policies" ],
|
|
58
58
|
"policy.manage" => [ "Policies", "Write, publish and re-issue policies" ],
|
|
59
|
+
"asset.view" => [ "Assets", "See company assets and who holds them" ],
|
|
60
|
+
"asset.manage" => [ "Assets", "Assign and take back company assets" ],
|
|
61
|
+
"checklist.manage" => [ "Lifecycle", "Work through joining and leaving checklists" ],
|
|
59
62
|
"settings.manage" => [ "Administration", "Change company settings, offices and policy" ],
|
|
60
63
|
"audit.view" => [ "Administration", "Read the audit trail" ],
|
|
61
64
|
"audit.view_money" => [ "Administration", "Read audit rows about pay, appraisals and promotions" ],
|
data/lib/hr_lite/role_seeds.rb
CHANGED
|
@@ -19,7 +19,7 @@ module HrLite
|
|
|
19
19
|
"profile.view" => "self", "appraisal.view" => "self",
|
|
20
20
|
"resignation.view" => "self", "document.view" => "self", "tax.view" => "self",
|
|
21
21
|
"expense.claim" => "self", "benefit.view" => "self",
|
|
22
|
-
"hr_request.raise" => "self", "policy.view" => "all"
|
|
22
|
+
"hr_request.raise" => "self", "policy.view" => "all", "asset.view" => "self"
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
Role::MANAGER => {
|
|
@@ -40,7 +40,8 @@ module HrLite
|
|
|
40
40
|
"appraisal.view" => "self", "resignation.view" => "all",
|
|
41
41
|
"document.view" => "all", "tax.view" => "self",
|
|
42
42
|
"expense.claim" => "self", "benefit.view" => "all", "benefit.manage" => "all",
|
|
43
|
-
"hr_request.raise" => "self", "hr_request.manage" => "all", "policy.view" => "all"
|
|
43
|
+
"hr_request.raise" => "self", "hr_request.manage" => "all", "policy.view" => "all",
|
|
44
|
+
"asset.view" => "all", "asset.manage" => "all", "checklist.manage" => "all"
|
|
44
45
|
}
|
|
45
46
|
},
|
|
46
47
|
Role::FINANCE => {
|
|
@@ -65,7 +66,8 @@ module HrLite
|
|
|
65
66
|
"resignation.view" => "all", "resignation.manage" => "all",
|
|
66
67
|
"settings.manage" => "all", "audit.view" => "all", "payroll.view" => "self",
|
|
67
68
|
"document.view" => "all", "expense.approve" => "all", "benefit.manage" => "all",
|
|
68
|
-
"hr_request.manage" => "all", "policy.view" => "all", "policy.manage" => "all"
|
|
69
|
+
"hr_request.manage" => "all", "policy.view" => "all", "policy.manage" => "all",
|
|
70
|
+
"asset.view" => "all", "asset.manage" => "all", "checklist.manage" => "all"
|
|
69
71
|
}
|
|
70
72
|
},
|
|
71
73
|
Role::SUPER_ADMIN => {
|
data/lib/hr_lite/version.rb
CHANGED
|
@@ -5,7 +5,8 @@ namespace :hr_lite do
|
|
|
5
5
|
require "hr_lite/role_seeds"
|
|
6
6
|
require "hr_lite/statutory_seeds"
|
|
7
7
|
created = HrLite::Seeds.run! + HrLite::RoleSeeds.call + HrLite::StatutorySeeds.call +
|
|
8
|
-
HrLite::SalaryComponent.seed_defaults!
|
|
8
|
+
HrLite::SalaryComponent.seed_defaults! +
|
|
9
|
+
HrLite::ChecklistTemplate.seed_defaults!
|
|
9
10
|
puts created.any? ? "hr_lite:seed created: #{created.join(', ')}" : "hr_lite:seed — nothing to do"
|
|
10
11
|
end
|
|
11
12
|
|
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
|
+
version: 0.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kshitiz sinha
|
|
@@ -56,13 +56,17 @@ files:
|
|
|
56
56
|
- app/assets/stylesheets/hr_lite/application.css
|
|
57
57
|
- app/assets/stylesheets/hr_lite/hr_lite.css
|
|
58
58
|
- app/controllers/hr_lite/admin/appraisals_controller.rb
|
|
59
|
+
- app/controllers/hr_lite/admin/assets_controller.rb
|
|
59
60
|
- app/controllers/hr_lite/admin/attendances_controller.rb
|
|
60
61
|
- app/controllers/hr_lite/admin/audit_logs_controller.rb
|
|
61
62
|
- app/controllers/hr_lite/admin/base_controller.rb
|
|
63
|
+
- app/controllers/hr_lite/admin/checklists_controller.rb
|
|
62
64
|
- app/controllers/hr_lite/admin/comp_off_requests_controller.rb
|
|
63
65
|
- app/controllers/hr_lite/admin/designation_changes_controller.rb
|
|
64
66
|
- app/controllers/hr_lite/admin/employees_controller.rb
|
|
67
|
+
- app/controllers/hr_lite/admin/expenses_controller.rb
|
|
65
68
|
- app/controllers/hr_lite/admin/holidays_controller.rb
|
|
69
|
+
- app/controllers/hr_lite/admin/hr_requests_controller.rb
|
|
66
70
|
- app/controllers/hr_lite/admin/leadership_controller.rb
|
|
67
71
|
- app/controllers/hr_lite/admin/leave_balances_controller.rb
|
|
68
72
|
- app/controllers/hr_lite/admin/leave_requests_controller.rb
|
|
@@ -121,10 +125,14 @@ files:
|
|
|
121
125
|
- app/models/hr_lite/approval_delegation.rb
|
|
122
126
|
- app/models/hr_lite/approval_flow.rb
|
|
123
127
|
- app/models/hr_lite/approval_step.rb
|
|
128
|
+
- app/models/hr_lite/asset.rb
|
|
129
|
+
- app/models/hr_lite/asset_assignment.rb
|
|
124
130
|
- app/models/hr_lite/attendance_record.rb
|
|
125
131
|
- app/models/hr_lite/audit_log.rb
|
|
126
132
|
- app/models/hr_lite/benefit.rb
|
|
127
133
|
- app/models/hr_lite/benefit_enrolment.rb
|
|
134
|
+
- app/models/hr_lite/checklist_item.rb
|
|
135
|
+
- app/models/hr_lite/checklist_template.rb
|
|
128
136
|
- app/models/hr_lite/comp_off_request.rb
|
|
129
137
|
- app/models/hr_lite/designation_change.rb
|
|
130
138
|
- app/models/hr_lite/document.rb
|
|
@@ -178,9 +186,12 @@ files:
|
|
|
178
186
|
- app/views/hr_lite/admin/appraisals/_form.html.erb
|
|
179
187
|
- app/views/hr_lite/admin/appraisals/edit.html.erb
|
|
180
188
|
- app/views/hr_lite/admin/appraisals/new.html.erb
|
|
189
|
+
- app/views/hr_lite/admin/assets/index.html.erb
|
|
190
|
+
- app/views/hr_lite/admin/assets/new.html.erb
|
|
181
191
|
- app/views/hr_lite/admin/attendances/index.html.erb
|
|
182
192
|
- app/views/hr_lite/admin/attendances/show.html.erb
|
|
183
193
|
- app/views/hr_lite/admin/audit_logs/index.html.erb
|
|
194
|
+
- app/views/hr_lite/admin/checklists/index.html.erb
|
|
184
195
|
- app/views/hr_lite/admin/comp_off_requests/index.html.erb
|
|
185
196
|
- app/views/hr_lite/admin/comp_off_requests/show.html.erb
|
|
186
197
|
- app/views/hr_lite/admin/designation_changes/new.html.erb
|
|
@@ -189,7 +200,10 @@ files:
|
|
|
189
200
|
- app/views/hr_lite/admin/employees/index.html.erb
|
|
190
201
|
- app/views/hr_lite/admin/employees/new.html.erb
|
|
191
202
|
- app/views/hr_lite/admin/employees/show.html.erb
|
|
203
|
+
- app/views/hr_lite/admin/expenses/index.html.erb
|
|
192
204
|
- app/views/hr_lite/admin/holidays/index.html.erb
|
|
205
|
+
- app/views/hr_lite/admin/hr_requests/index.html.erb
|
|
206
|
+
- app/views/hr_lite/admin/hr_requests/show.html.erb
|
|
193
207
|
- app/views/hr_lite/admin/leave_balances/index.html.erb
|
|
194
208
|
- app/views/hr_lite/admin/leave_requests/index.html.erb
|
|
195
209
|
- app/views/hr_lite/admin/leave_requests/show.html.erb
|
|
@@ -306,6 +320,7 @@ files:
|
|
|
306
320
|
- db/migrate/20260818002627_create_hr_lite_payroll_components_and_loans.rb
|
|
307
321
|
- db/migrate/20260818003616_create_hr_lite_documents_and_declarations.rb
|
|
308
322
|
- db/migrate/20260818004707_create_hr_lite_employee_services.rb
|
|
323
|
+
- db/migrate/20260818010401_create_hr_lite_assets_and_onboarding.rb
|
|
309
324
|
- lib/generators/hr_lite/install/install_generator.rb
|
|
310
325
|
- lib/generators/hr_lite/install/templates/AFTER_INSTALL
|
|
311
326
|
- lib/generators/hr_lite/install/templates/initializer.rb
|