hr_lite 0.12.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 +67 -1
- data/app/assets/stylesheets/hr_lite/hr_lite.css +6 -0
- 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/controllers/hr_lite/admin/reports_controller.rb +151 -0
- data/app/controllers/hr_lite/benefits_controller.rb +17 -0
- data/app/controllers/hr_lite/expenses_controller.rb +50 -0
- data/app/controllers/hr_lite/hr_requests_controller.rb +43 -0
- data/app/controllers/hr_lite/policies_controller.rb +26 -0
- data/app/helpers/hr_lite/application_helper.rb +10 -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/app/views/hr_lite/admin/reports/index.html.erb +40 -0
- data/app/views/hr_lite/admin/reports/show.html.erb +32 -0
- data/app/views/hr_lite/benefits/index.html.erb +33 -0
- data/app/views/hr_lite/expenses/index.html.erb +68 -0
- data/app/views/hr_lite/expenses/new.html.erb +38 -0
- data/app/views/hr_lite/hr_requests/index.html.erb +39 -0
- data/app/views/hr_lite/hr_requests/new.html.erb +28 -0
- data/app/views/hr_lite/hr_requests/show.html.erb +25 -0
- data/app/views/hr_lite/policies/index.html.erb +34 -0
- data/app/views/hr_lite/policies/show.html.erb +23 -0
- data/config/routes.rb +23 -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 +31 -1
|
@@ -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 %>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Reports" } %>
|
|
2
|
+
<div class="hrl-page__head"><h1 class="hrl-page__title">Reports</h1></div>
|
|
3
|
+
|
|
4
|
+
<section class="hrl-card">
|
|
5
|
+
<p class="hrl-muted hrl-small">
|
|
6
|
+
Each report reaches exactly as far as the screen it summarises — a manager
|
|
7
|
+
sees their own team, HR sees everybody.
|
|
8
|
+
</p>
|
|
9
|
+
<%= form_with url: hr_lite.admin_reports_path, method: :get, local: true do %>
|
|
10
|
+
<div class="hrl-field">
|
|
11
|
+
<%= label_tag :month, "Month" %>
|
|
12
|
+
<%= text_field_tag :month, @month.strftime("%Y-%m"), placeholder: "YYYY-MM" %>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="hrl-form-actions">
|
|
15
|
+
<%= submit_tag "Change month", class: "hrl-btn" %>
|
|
16
|
+
</div>
|
|
17
|
+
<% end %>
|
|
18
|
+
</section>
|
|
19
|
+
|
|
20
|
+
<section class="hrl-card">
|
|
21
|
+
<div class="hrl-table-wrap">
|
|
22
|
+
<table class="hrl-table hrl-table--stack">
|
|
23
|
+
<thead><tr><th>Report</th><th></th></tr></thead>
|
|
24
|
+
<tbody>
|
|
25
|
+
<% @reports.each do |name, label| %>
|
|
26
|
+
<tr>
|
|
27
|
+
<td data-label="Report"><%= label %></td>
|
|
28
|
+
<td data-label="">
|
|
29
|
+
<a class="hrl-small"
|
|
30
|
+
href="<%= hr_lite.admin_report_path(name, month: @month.strftime("%Y-%m")) %>">Open</a>
|
|
31
|
+
|
|
32
|
+
<a class="hrl-small"
|
|
33
|
+
href="<%= hr_lite.admin_report_path(name, month: @month.strftime("%Y-%m"), format: :csv) %>">CSV</a>
|
|
34
|
+
</td>
|
|
35
|
+
</tr>
|
|
36
|
+
<% end %>
|
|
37
|
+
</tbody>
|
|
38
|
+
</table>
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<% content_for(:page_title) { @reports_title = HrLite::Admin::ReportsController::REPORTS[@name] } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title"><%= HrLite::Admin::ReportsController::REPORTS[@name] %></h1>
|
|
4
|
+
<div class="hrl-page__actions">
|
|
5
|
+
<a class="hrl-btn"
|
|
6
|
+
href="<%= hr_lite.admin_report_path(@name, month: @month.strftime("%Y-%m"), format: :csv) %>">Download CSV</a>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<section class="hrl-card">
|
|
11
|
+
<p class="hrl-small hrl-muted"><%= @month.strftime("%B %Y") %></p>
|
|
12
|
+
<% if @rows.empty? %>
|
|
13
|
+
<p class="hrl-muted">Nothing to report for this month.</p>
|
|
14
|
+
<% else %>
|
|
15
|
+
<div class="hrl-table-wrap">
|
|
16
|
+
<table class="hrl-table hrl-table--stack">
|
|
17
|
+
<thead>
|
|
18
|
+
<tr><% @rows.first.keys.each do |header| %><th><%= header %></th><% end %></tr>
|
|
19
|
+
</thead>
|
|
20
|
+
<tbody>
|
|
21
|
+
<% @rows.each do |row| %>
|
|
22
|
+
<tr>
|
|
23
|
+
<% row.each do |header, value| %>
|
|
24
|
+
<td data-label="<%= header %>"><%= value %></td>
|
|
25
|
+
<% end %>
|
|
26
|
+
</tr>
|
|
27
|
+
<% end %>
|
|
28
|
+
</tbody>
|
|
29
|
+
</table>
|
|
30
|
+
</div>
|
|
31
|
+
<% end %>
|
|
32
|
+
</section>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Benefits" } %>
|
|
2
|
+
<div class="hrl-page__head"><h1 class="hrl-page__title">Your benefits</h1></div>
|
|
3
|
+
|
|
4
|
+
<section class="hrl-card">
|
|
5
|
+
<% if @enrolments.empty? %>
|
|
6
|
+
<p class="hrl-muted">You are not enrolled in any benefit yet.</p>
|
|
7
|
+
<% else %>
|
|
8
|
+
<div class="hrl-table-wrap">
|
|
9
|
+
<table class="hrl-table hrl-table--stack">
|
|
10
|
+
<thead><tr><th>Benefit</th><th>Cover</th><th>Since</th><th>Dependants</th></tr></thead>
|
|
11
|
+
<tbody>
|
|
12
|
+
<% @enrolments.each do |enrolment| %>
|
|
13
|
+
<% benefit = enrolment.benefit %>
|
|
14
|
+
<tr>
|
|
15
|
+
<td data-label="Benefit">
|
|
16
|
+
<strong><%= benefit.name %></strong>
|
|
17
|
+
<div class="hrl-small hrl-muted">
|
|
18
|
+
<%= benefit.kind.humanize %><%= " · #{benefit.provider}" if benefit.provider.present? %>
|
|
19
|
+
<%= " · policy #{benefit.policy_number}" if benefit.policy_number.present? %>
|
|
20
|
+
</div>
|
|
21
|
+
</td>
|
|
22
|
+
<td data-label="Cover" class="hrl-num">
|
|
23
|
+
<%= benefit.coverage ? hrl_money(benefit.coverage) : "—" %>
|
|
24
|
+
</td>
|
|
25
|
+
<td data-label="Since"><%= enrolment.enrolled_on.strftime("%d %b %Y") %></td>
|
|
26
|
+
<td data-label="Dependants" class="hrl-num"><%= enrolment.dependants %></td>
|
|
27
|
+
</tr>
|
|
28
|
+
<% end %>
|
|
29
|
+
</tbody>
|
|
30
|
+
</table>
|
|
31
|
+
</div>
|
|
32
|
+
<% end %>
|
|
33
|
+
</section>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Expenses" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Expenses</h1>
|
|
4
|
+
<div class="hrl-page__actions">
|
|
5
|
+
<a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_expense_path %>">New claim</a>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<% if @categories.any? %>
|
|
10
|
+
<section class="hrl-card">
|
|
11
|
+
<h2 class="hrl-card__title">What is left this month</h2>
|
|
12
|
+
<ul>
|
|
13
|
+
<% @categories.each do |category| %>
|
|
14
|
+
<% remaining = category.remaining_for(hr_current_user) %>
|
|
15
|
+
<li class="hrl-small">
|
|
16
|
+
<strong><%= category.name %></strong> —
|
|
17
|
+
<%= remaining.nil? ? "no cap" : "#{hrl_money(remaining)} left" %>
|
|
18
|
+
<% if category.receipt_required %>
|
|
19
|
+
<span class="hrl-badge hrl-badge--muted">Receipt needed</span>
|
|
20
|
+
<% end %>
|
|
21
|
+
</li>
|
|
22
|
+
<% end %>
|
|
23
|
+
</ul>
|
|
24
|
+
</section>
|
|
25
|
+
<% end %>
|
|
26
|
+
|
|
27
|
+
<section class="hrl-card">
|
|
28
|
+
<% if @expenses.empty? %>
|
|
29
|
+
<p class="hrl-muted">No claims yet.</p>
|
|
30
|
+
<% else %>
|
|
31
|
+
<div class="hrl-table-wrap">
|
|
32
|
+
<table class="hrl-table hrl-table--stack">
|
|
33
|
+
<thead><tr><th>Spent</th><th>Category</th><th>Amount</th><th>Status</th><th></th></tr></thead>
|
|
34
|
+
<tbody>
|
|
35
|
+
<% @expenses.each do |expense| %>
|
|
36
|
+
<tr>
|
|
37
|
+
<td data-label="Spent"><%= expense.spent_on.strftime("%d %b %Y") %></td>
|
|
38
|
+
<td data-label="Category">
|
|
39
|
+
<%= expense.category.name %>
|
|
40
|
+
<div class="hrl-small hrl-muted"><%= expense.description %></div>
|
|
41
|
+
</td>
|
|
42
|
+
<td data-label="Amount" class="hrl-num"><%= hrl_money(expense.amount) %></td>
|
|
43
|
+
<td data-label="Status">
|
|
44
|
+
<%= hrl_request_status_badge(expense.status) %>
|
|
45
|
+
<% if expense.reimbursed_in %>
|
|
46
|
+
<div class="hrl-small hrl-muted">
|
|
47
|
+
with <%= expense.reimbursed_in.strftime("%B %Y") %> payroll
|
|
48
|
+
</div>
|
|
49
|
+
<% end %>
|
|
50
|
+
<% if expense.decision_note.present? %>
|
|
51
|
+
<div class="hrl-small hrl-muted"><%= expense.decision_note %></div>
|
|
52
|
+
<% end %>
|
|
53
|
+
</td>
|
|
54
|
+
<td data-label="">
|
|
55
|
+
<% if expense.draft? || expense.submitted? %>
|
|
56
|
+
<%= button_to "Withdraw", hr_lite.cancel_expense_path(expense),
|
|
57
|
+
class: "hrl-btn",
|
|
58
|
+
form: { data: { turbo_confirm: "Withdraw this claim?" } } %>
|
|
59
|
+
<% end %>
|
|
60
|
+
</td>
|
|
61
|
+
</tr>
|
|
62
|
+
<% end %>
|
|
63
|
+
</tbody>
|
|
64
|
+
</table>
|
|
65
|
+
</div>
|
|
66
|
+
<%= hrl_pagination %>
|
|
67
|
+
<% end %>
|
|
68
|
+
</section>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<% content_for(:page_title) { "New claim" } %>
|
|
2
|
+
<div class="hrl-page__head"><h1 class="hrl-page__title">New claim</h1></div>
|
|
3
|
+
|
|
4
|
+
<section class="hrl-card">
|
|
5
|
+
<%= form_with model: @expense, url: hr_lite.expenses_path, method: :post, local: true do |f| %>
|
|
6
|
+
<%= render "hr_lite/shared/form_errors", record: @expense %>
|
|
7
|
+
|
|
8
|
+
<div class="hrl-field">
|
|
9
|
+
<%= f.label :category_id, "What kind of expense" %>
|
|
10
|
+
<%= f.select :category_id, @categories.map { |c| [ c.name, c.id ] },
|
|
11
|
+
{ include_blank: "Choose one" } %>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="hrl-field">
|
|
14
|
+
<%= f.label :amount, "Amount (₹)" %>
|
|
15
|
+
<%= f.text_field :amount, inputmode: "decimal" %>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="hrl-field">
|
|
18
|
+
<%= f.label :spent_on, "When you spent it" %>
|
|
19
|
+
<%= f.date_field :spent_on, max: Date.current.to_s %>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="hrl-field">
|
|
22
|
+
<%= f.label :description, "What it was for" %>
|
|
23
|
+
<%= f.text_field :description %>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="hrl-field">
|
|
26
|
+
<%= f.label :receipt, "Receipt (PDF or photo)" %>
|
|
27
|
+
<%= f.file_field :receipt, accept: "application/pdf,image/*" %>
|
|
28
|
+
<span class="hrl-small hrl-muted">
|
|
29
|
+
Some categories need one — the form will say so if yours does.
|
|
30
|
+
</span>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="hrl-form-actions">
|
|
34
|
+
<%= f.submit "Submit claim", class: "hrl-btn hrl-btn--primary" %>
|
|
35
|
+
<a class="hrl-btn" href="<%= hr_lite.expenses_path %>">Cancel</a>
|
|
36
|
+
</div>
|
|
37
|
+
<% end %>
|
|
38
|
+
</section>
|