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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -1
  3. data/app/controllers/hr_lite/admin/assets_controller.rb +53 -0
  4. data/app/controllers/hr_lite/admin/checklists_controller.rb +32 -0
  5. data/app/controllers/hr_lite/admin/documents_controller.rb +63 -0
  6. data/app/controllers/hr_lite/admin/employees_controller.rb +4 -0
  7. data/app/controllers/hr_lite/admin/expenses_controller.rb +45 -0
  8. data/app/controllers/hr_lite/admin/hr_requests_controller.rb +38 -0
  9. data/app/controllers/hr_lite/admin/loans_controller.rb +59 -0
  10. data/app/controllers/hr_lite/admin/tax_declarations_controller.rb +75 -0
  11. data/app/controllers/hr_lite/documents_controller.rb +69 -0
  12. data/app/controllers/hr_lite/loans_controller.rb +11 -0
  13. data/app/controllers/hr_lite/tax_declarations_controller.rb +100 -0
  14. data/app/helpers/hr_lite/application_helper.rb +14 -4
  15. data/app/models/hr_lite/approval_flow.rb +1 -1
  16. data/app/models/hr_lite/asset.rb +57 -0
  17. data/app/models/hr_lite/asset_assignment.rb +24 -0
  18. data/app/models/hr_lite/checklist_item.rb +43 -0
  19. data/app/models/hr_lite/checklist_template.rb +50 -0
  20. data/app/models/hr_lite/employee_profile.rb +13 -0
  21. data/app/models/hr_lite/tax_declaration.rb +4 -1
  22. data/app/views/hr_lite/admin/assets/index.html.erb +59 -0
  23. data/app/views/hr_lite/admin/assets/new.html.erb +23 -0
  24. data/app/views/hr_lite/admin/checklists/index.html.erb +50 -0
  25. data/app/views/hr_lite/admin/documents/index.html.erb +89 -0
  26. data/app/views/hr_lite/admin/documents/show.html.erb +42 -0
  27. data/app/views/hr_lite/admin/expenses/index.html.erb +54 -0
  28. data/app/views/hr_lite/admin/hr_requests/index.html.erb +41 -0
  29. data/app/views/hr_lite/admin/hr_requests/show.html.erb +47 -0
  30. data/app/views/hr_lite/admin/loans/index.html.erb +43 -0
  31. data/app/views/hr_lite/admin/loans/new.html.erb +39 -0
  32. data/app/views/hr_lite/admin/loans/show.html.erb +52 -0
  33. data/app/views/hr_lite/admin/tax_declarations/index.html.erb +37 -0
  34. data/app/views/hr_lite/admin/tax_declarations/show.html.erb +54 -0
  35. data/app/views/hr_lite/documents/index.html.erb +84 -0
  36. data/app/views/hr_lite/loans/index.html.erb +52 -0
  37. data/app/views/hr_lite/tax_declarations/show.html.erb +68 -0
  38. data/config/routes.rb +29 -0
  39. data/db/migrate/20260818010401_create_hr_lite_assets_and_onboarding.rb +76 -0
  40. data/lib/hr_lite/permissions.rb +3 -0
  41. data/lib/hr_lite/role_seeds.rb +5 -3
  42. data/lib/hr_lite/version.rb +1 -1
  43. data/lib/tasks/hr_lite_tasks.rake +2 -1
  44. 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 &amp; 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>