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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -1
  3. data/app/assets/stylesheets/hr_lite/hr_lite.css +6 -0
  4. data/app/controllers/hr_lite/admin/assets_controller.rb +53 -0
  5. data/app/controllers/hr_lite/admin/checklists_controller.rb +32 -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/reports_controller.rb +151 -0
  10. data/app/controllers/hr_lite/benefits_controller.rb +17 -0
  11. data/app/controllers/hr_lite/expenses_controller.rb +50 -0
  12. data/app/controllers/hr_lite/hr_requests_controller.rb +43 -0
  13. data/app/controllers/hr_lite/policies_controller.rb +26 -0
  14. data/app/helpers/hr_lite/application_helper.rb +10 -1
  15. data/app/models/hr_lite/asset.rb +57 -0
  16. data/app/models/hr_lite/asset_assignment.rb +24 -0
  17. data/app/models/hr_lite/checklist_item.rb +43 -0
  18. data/app/models/hr_lite/checklist_template.rb +50 -0
  19. data/app/models/hr_lite/employee_profile.rb +13 -0
  20. data/app/views/hr_lite/admin/assets/index.html.erb +59 -0
  21. data/app/views/hr_lite/admin/assets/new.html.erb +23 -0
  22. data/app/views/hr_lite/admin/checklists/index.html.erb +50 -0
  23. data/app/views/hr_lite/admin/expenses/index.html.erb +54 -0
  24. data/app/views/hr_lite/admin/hr_requests/index.html.erb +41 -0
  25. data/app/views/hr_lite/admin/hr_requests/show.html.erb +47 -0
  26. data/app/views/hr_lite/admin/reports/index.html.erb +40 -0
  27. data/app/views/hr_lite/admin/reports/show.html.erb +32 -0
  28. data/app/views/hr_lite/benefits/index.html.erb +33 -0
  29. data/app/views/hr_lite/expenses/index.html.erb +68 -0
  30. data/app/views/hr_lite/expenses/new.html.erb +38 -0
  31. data/app/views/hr_lite/hr_requests/index.html.erb +39 -0
  32. data/app/views/hr_lite/hr_requests/new.html.erb +28 -0
  33. data/app/views/hr_lite/hr_requests/show.html.erb +25 -0
  34. data/app/views/hr_lite/policies/index.html.erb +34 -0
  35. data/app/views/hr_lite/policies/show.html.erb +23 -0
  36. data/config/routes.rb +23 -0
  37. data/db/migrate/20260818010401_create_hr_lite_assets_and_onboarding.rb +76 -0
  38. data/lib/hr_lite/permissions.rb +3 -0
  39. data/lib/hr_lite/role_seeds.rb +5 -3
  40. data/lib/hr_lite/version.rb +1 -1
  41. data/lib/tasks/hr_lite_tasks.rake +2 -1
  42. 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 &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,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
+ &nbsp;
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>