hr_lite 0.13.0 → 0.15.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 +86 -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/documents_controller.rb +63 -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/controllers/hr_lite/admin/loans_controller.rb +59 -0
- data/app/controllers/hr_lite/admin/tax_declarations_controller.rb +75 -0
- data/app/controllers/hr_lite/documents_controller.rb +69 -0
- data/app/controllers/hr_lite/loans_controller.rb +11 -0
- data/app/controllers/hr_lite/tax_declarations_controller.rb +100 -0
- data/app/helpers/hr_lite/application_helper.rb +14 -4
- data/app/models/hr_lite/approval_flow.rb +1 -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/models/hr_lite/tax_declaration.rb +4 -1
- 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/documents/index.html.erb +89 -0
- data/app/views/hr_lite/admin/documents/show.html.erb +42 -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/app/views/hr_lite/admin/loans/index.html.erb +43 -0
- data/app/views/hr_lite/admin/loans/new.html.erb +39 -0
- data/app/views/hr_lite/admin/loans/show.html.erb +52 -0
- data/app/views/hr_lite/admin/tax_declarations/index.html.erb +37 -0
- data/app/views/hr_lite/admin/tax_declarations/show.html.erb +54 -0
- data/app/views/hr_lite/documents/index.html.erb +84 -0
- data/app/views/hr_lite/loans/index.html.erb +52 -0
- data/app/views/hr_lite/tax_declarations/show.html.erb +68 -0
- data/config/routes.rb +29 -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 +32 -1
|
@@ -19,20 +19,28 @@ module HrLite
|
|
|
19
19
|
{ label: "Org", path: :org_path, match: [ "/org" ] },
|
|
20
20
|
{ label: "Kudos", path: :kudos_path, match: [ "/kudos" ] },
|
|
21
21
|
{ label: "Slips", path: :salary_slips_path, match: [ "/salary_slips" ] },
|
|
22
|
-
{ label: "Career", path: :career_path, match: [ "/career", "/appraisals", "/profile" ] }
|
|
22
|
+
{ label: "Career", path: :career_path, match: [ "/career", "/appraisals", "/profile" ] },
|
|
23
|
+
{ label: "Documents", path: :documents_path, match: [ "/documents" ] },
|
|
24
|
+
{ label: "Tax", path: :tax_declaration_path, match: [ "/tax_declaration" ] },
|
|
25
|
+
{ label: "Loans", path: :loans_path, match: [ "/loans" ] }
|
|
23
26
|
].freeze
|
|
24
27
|
|
|
25
28
|
ADMIN_NAV_ITEMS = [
|
|
26
29
|
{ label: "Overview", path: :admin_overview_path, match: [ "/admin/overview" ] },
|
|
27
30
|
{ label: "Team attendance", path: :admin_attendances_path, match: [ "/admin/attendances" ] },
|
|
28
31
|
{ 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" ] }
|
|
32
|
+
{ label: "Reports", path: :admin_reports_path, match: [ "/admin/reports" ] },
|
|
33
|
+
{ label: "Claims", path: :admin_expenses_path, match: [ "/admin/expenses" ] },
|
|
34
|
+
{ label: "Help desk", path: :admin_hr_requests_path, match: [ "/admin/hr_requests" ] },
|
|
35
|
+
{ label: "Assets", path: :admin_assets_path, match: [ "/admin/assets" ] },
|
|
36
|
+
{ label: "Joining & exits", path: :admin_checklists_path, match: [ "/admin/checklists" ] }
|
|
30
37
|
].freeze
|
|
31
38
|
|
|
32
39
|
LEADERSHIP_NAV_ITEMS = [
|
|
33
40
|
{ label: "Employees", path: :admin_employees_path, match: [ "/admin/employees" ] },
|
|
34
41
|
{ label: "Settings", path: :admin_leave_types_path, match: [ "/admin/leave_types", "/admin/office_locations", "/admin/holidays", "/admin/setting" ] },
|
|
35
|
-
{ label: "Audit", path: :admin_audit_logs_path, match: [ "/admin/audit_logs" ] }
|
|
42
|
+
{ label: "Audit", path: :admin_audit_logs_path, match: [ "/admin/audit_logs" ] },
|
|
43
|
+
{ label: "Documents", path: :admin_documents_path, match: [ "/admin/documents" ] }
|
|
36
44
|
].freeze
|
|
37
45
|
|
|
38
46
|
# Money screens, plus role management: granting somebody payroll is the
|
|
@@ -41,7 +49,9 @@ module HrLite
|
|
|
41
49
|
SUPERADMIN_NAV_ITEMS = [
|
|
42
50
|
{ label: "Payroll", path: :admin_payroll_runs_path, match: [ "/admin/payroll_runs", "/admin/salary_slips" ] },
|
|
43
51
|
{ label: "Roles", path: :admin_roles_path, match: [ "/admin/roles" ] },
|
|
44
|
-
{ label: "Rates", path: :admin_statutory_rate_cards_path, match: [ "/admin/statutory_rate_cards" ] }
|
|
52
|
+
{ label: "Rates", path: :admin_statutory_rate_cards_path, match: [ "/admin/statutory_rate_cards" ] },
|
|
53
|
+
{ label: "Tax", path: :admin_tax_declarations_path, match: [ "/admin/tax_declarations" ] },
|
|
54
|
+
{ label: "Loans", path: :admin_loans_path, match: [ "/admin/loans" ] }
|
|
45
55
|
].freeze
|
|
46
56
|
|
|
47
57
|
# Only items whose routes exist yet (the nav grows with each phase).
|
|
@@ -4,7 +4,7 @@ module HrLite
|
|
|
4
4
|
class ApprovalFlow < ApplicationRecord
|
|
5
5
|
include Audited
|
|
6
6
|
|
|
7
|
-
has_many :approval_steps, -> { order(:position) },
|
|
7
|
+
has_many :approval_steps, -> { order(:position) }, inverse_of: :flow,
|
|
8
8
|
class_name: "HrLite::ApprovalStep", foreign_key: :flow_id, dependent: :destroy
|
|
9
9
|
accepts_nested_attributes_for :approval_steps, allow_destroy: true
|
|
10
10
|
|
|
@@ -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
|
|
@@ -11,8 +11,11 @@ module HrLite
|
|
|
11
11
|
STATUSES = %w[draft submitted verified rejected].freeze
|
|
12
12
|
REGIMES = %w[new old].freeze
|
|
13
13
|
|
|
14
|
+
# inverse_of is load-bearing, not decoration: the foreign key is not
|
|
15
|
+
# derived from the class name, so without it a nested item on an UNSAVED
|
|
16
|
+
# declaration cannot see its parent and fails "declaration must exist".
|
|
14
17
|
has_many :tax_declaration_items, -> { order(:section) },
|
|
15
|
-
class_name: "HrLite::TaxDeclarationItem",
|
|
18
|
+
class_name: "HrLite::TaxDeclarationItem", inverse_of: :declaration,
|
|
16
19
|
foreign_key: :declaration_id, dependent: :destroy
|
|
17
20
|
accepts_nested_attributes_for :tax_declaration_items, allow_destroy: true
|
|
18
21
|
|
|
@@ -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,89 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Documents" } %>
|
|
2
|
+
<div class="hrl-page__head"><h1 class="hrl-page__title">Documents</h1></div>
|
|
3
|
+
|
|
4
|
+
<section class="hrl-card">
|
|
5
|
+
<p class="hrl-muted hrl-small">
|
|
6
|
+
Seeing that a document exists and opening it are different things — a
|
|
7
|
+
passport is visible here, but the file itself only opens for whoever its
|
|
8
|
+
visibility allows.
|
|
9
|
+
</p>
|
|
10
|
+
<div class="hrl-row">
|
|
11
|
+
<a class="hrl-btn <%= "hrl-btn--primary" if @category.nil? %>"
|
|
12
|
+
href="<%= hr_lite.admin_documents_path %>">All</a>
|
|
13
|
+
<% @categories.each do |category| %>
|
|
14
|
+
<a class="hrl-btn <%= "hrl-btn--primary" if @category == category %>"
|
|
15
|
+
href="<%= hr_lite.admin_documents_path(category: category) %>"><%= category.humanize %></a>
|
|
16
|
+
<% end %>
|
|
17
|
+
</div>
|
|
18
|
+
</section>
|
|
19
|
+
|
|
20
|
+
<section class="hrl-card">
|
|
21
|
+
<h2 class="hrl-card__title">Waiting to be checked (<%= @pending.size %>)</h2>
|
|
22
|
+
<% if @pending.empty? %>
|
|
23
|
+
<p class="hrl-muted">Nothing pending.</p>
|
|
24
|
+
<% else %>
|
|
25
|
+
<div class="hrl-table-wrap">
|
|
26
|
+
<table class="hrl-table hrl-table--stack">
|
|
27
|
+
<thead><tr><th>Person</th><th>Document</th><th>File</th><th></th></tr></thead>
|
|
28
|
+
<tbody>
|
|
29
|
+
<% @pending.each do |doc| %>
|
|
30
|
+
<tr>
|
|
31
|
+
<td data-label="Person"><%= hr_display_name(doc.user) %></td>
|
|
32
|
+
<td data-label="Document">
|
|
33
|
+
<%= doc.title %>
|
|
34
|
+
<div class="hrl-small hrl-muted"><%= doc.category.humanize %></div>
|
|
35
|
+
</td>
|
|
36
|
+
<td data-label="File">
|
|
37
|
+
<% if doc.readable_by?(hr_current_user) && doc.file.attached? %>
|
|
38
|
+
<a class="hrl-small" href="<%= hr_lite.download_document_path(doc) %>">Open</a>
|
|
39
|
+
<% else %>
|
|
40
|
+
<span class="hrl-small hrl-muted">Not yours to open</span>
|
|
41
|
+
<% end %>
|
|
42
|
+
</td>
|
|
43
|
+
<td data-label="">
|
|
44
|
+
<%= button_to "Verify", hr_lite.verify_admin_document_path(doc),
|
|
45
|
+
method: :post, class: "hrl-btn hrl-btn--primary" %>
|
|
46
|
+
<%= form_with url: hr_lite.reject_admin_document_path(doc), method: :post, local: true do %>
|
|
47
|
+
<%= text_field_tag :note, nil, placeholder: "What is wrong with it" %>
|
|
48
|
+
<%= submit_tag "Reject", class: "hrl-btn" %>
|
|
49
|
+
<% end %>
|
|
50
|
+
</td>
|
|
51
|
+
</tr>
|
|
52
|
+
<% end %>
|
|
53
|
+
</tbody>
|
|
54
|
+
</table>
|
|
55
|
+
</div>
|
|
56
|
+
<% end %>
|
|
57
|
+
</section>
|
|
58
|
+
|
|
59
|
+
<section class="hrl-card">
|
|
60
|
+
<h2 class="hrl-card__title">Everything on file</h2>
|
|
61
|
+
<div class="hrl-table-wrap">
|
|
62
|
+
<table class="hrl-table hrl-table--stack">
|
|
63
|
+
<thead><tr><th>Person</th><th>Document</th><th>Expires</th><th>Checked</th></tr></thead>
|
|
64
|
+
<tbody>
|
|
65
|
+
<% @documents.each do |doc| %>
|
|
66
|
+
<tr>
|
|
67
|
+
<td data-label="Person">
|
|
68
|
+
<a href="<%= hr_lite.admin_employee_documents_path(user_id: doc.user_id) %>">
|
|
69
|
+
<%= hr_display_name(doc.user) %>
|
|
70
|
+
</a>
|
|
71
|
+
</td>
|
|
72
|
+
<td data-label="Document"><%= doc.title %></td>
|
|
73
|
+
<td data-label="Expires">
|
|
74
|
+
<% if doc.expires_on %>
|
|
75
|
+
<span class="<%= "hrl-badge hrl-badge--bad" if doc.expired? %>">
|
|
76
|
+
<%= doc.expires_on.strftime("%d %b %Y") %>
|
|
77
|
+
</span>
|
|
78
|
+
<% else %>—<% end %>
|
|
79
|
+
</td>
|
|
80
|
+
<td data-label="Checked">
|
|
81
|
+
<%= hrl_request_status_badge(doc.verification == "verified" ? "approved" : doc.verification) %>
|
|
82
|
+
</td>
|
|
83
|
+
</tr>
|
|
84
|
+
<% end %>
|
|
85
|
+
</tbody>
|
|
86
|
+
</table>
|
|
87
|
+
</div>
|
|
88
|
+
<%= hrl_pagination %>
|
|
89
|
+
</section>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<% content_for(:page_title) { hr_display_name(@employee) } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title"><%= hr_display_name(@employee) %> — documents</h1>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<% if @missing.any? %>
|
|
7
|
+
<section class="hrl-card hrl-card--warn">
|
|
8
|
+
<h2 class="hrl-card__title">Not on file</h2>
|
|
9
|
+
<p><%= @missing.map(&:humanize).to_sentence %></p>
|
|
10
|
+
</section>
|
|
11
|
+
<% end %>
|
|
12
|
+
|
|
13
|
+
<section class="hrl-card">
|
|
14
|
+
<% if @documents.empty? %>
|
|
15
|
+
<p class="hrl-muted">Nothing uploaded.</p>
|
|
16
|
+
<% else %>
|
|
17
|
+
<div class="hrl-table-wrap">
|
|
18
|
+
<table class="hrl-table hrl-table--stack">
|
|
19
|
+
<thead><tr><th>Document</th><th>Expires</th><th>Checked</th><th></th></tr></thead>
|
|
20
|
+
<tbody>
|
|
21
|
+
<% @documents.each do |doc| %>
|
|
22
|
+
<tr>
|
|
23
|
+
<td data-label="Document">
|
|
24
|
+
<strong><%= doc.title %></strong>
|
|
25
|
+
<div class="hrl-small hrl-muted"><%= doc.category.humanize %></div>
|
|
26
|
+
</td>
|
|
27
|
+
<td data-label="Expires"><%= doc.expires_on&.strftime("%d %b %Y") || "—" %></td>
|
|
28
|
+
<td data-label="Checked">
|
|
29
|
+
<%= hrl_request_status_badge(doc.verification == "verified" ? "approved" : doc.verification) %>
|
|
30
|
+
</td>
|
|
31
|
+
<td data-label="">
|
|
32
|
+
<% if doc.readable_by?(hr_current_user) && doc.file.attached? %>
|
|
33
|
+
<a class="hrl-small" href="<%= hr_lite.download_document_path(doc) %>">Open</a>
|
|
34
|
+
<% end %>
|
|
35
|
+
</td>
|
|
36
|
+
</tr>
|
|
37
|
+
<% end %>
|
|
38
|
+
</tbody>
|
|
39
|
+
</table>
|
|
40
|
+
</div>
|
|
41
|
+
<% end %>
|
|
42
|
+
</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>
|