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
@@ -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,43 @@
1
+ <% content_for(:page_title) { "Loans" } %>
2
+ <div class="hrl-page__head">
3
+ <h1 class="hrl-page__title">Loans &amp; advances</h1>
4
+ <div class="hrl-page__actions">
5
+ <a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_admin_loan_path %>">Record a loan</a>
6
+ </div>
7
+ </div>
8
+
9
+ <section class="hrl-card">
10
+ <p>Outstanding across every active loan: <strong><%= hrl_money(@outstanding) %></strong></p>
11
+ <div class="hrl-row">
12
+ <% HrLite::Loan::STATUSES.each do |status| %>
13
+ <a class="hrl-btn <%= "hrl-btn--primary" if @status == status %>"
14
+ href="<%= hr_lite.admin_loans_path(status: status) %>"><%= status.humanize %></a>
15
+ <% end %>
16
+ </div>
17
+ </section>
18
+
19
+ <section class="hrl-card">
20
+ <% if @loans.empty? %>
21
+ <p class="hrl-muted">Nothing <%= @status %>.</p>
22
+ <% else %>
23
+ <div class="hrl-table-wrap">
24
+ <table class="hrl-table hrl-table--stack">
25
+ <thead><tr><th>Person</th><th>Principal</th><th>Instalment</th><th>Outstanding</th><th></th></tr></thead>
26
+ <tbody>
27
+ <% @loans.each do |loan| %>
28
+ <tr>
29
+ <td data-label="Person"><%= hr_display_name(loan.user) %></td>
30
+ <td data-label="Principal" class="hrl-num"><%= hrl_money(loan.principal) %></td>
31
+ <td data-label="Instalment" class="hrl-num"><%= hrl_money(loan.monthly_instalment) %></td>
32
+ <td data-label="Outstanding" class="hrl-num"><%= hrl_money(loan.outstanding) %></td>
33
+ <td data-label="">
34
+ <a class="hrl-small" href="<%= hr_lite.admin_loan_path(loan) %>">Open</a>
35
+ </td>
36
+ </tr>
37
+ <% end %>
38
+ </tbody>
39
+ </table>
40
+ </div>
41
+ <% end %>
42
+ <%= hrl_pagination %>
43
+ </section>
@@ -0,0 +1,39 @@
1
+ <% content_for(:page_title) { "Record a loan" } %>
2
+ <div class="hrl-page__head"><h1 class="hrl-page__title">Record a loan</h1></div>
3
+
4
+ <section class="hrl-card">
5
+ <p class="hrl-muted hrl-small">
6
+ The instalment comes out of the payslip automatically from the month it
7
+ starts, and stops on its own when the balance runs out — the last
8
+ instalment is whatever is left, not a full one.
9
+ </p>
10
+ <%= form_with model: @loan, url: hr_lite.admin_loans_path, method: :post, local: true do |f| %>
11
+ <%= render "hr_lite/shared/form_errors", record: @loan %>
12
+ <div class="hrl-field">
13
+ <%= f.label :user_id, "Employee" %>
14
+ <%= f.select :user_id, options_for_select(HrLite.employees.map { |u| [ hr_display_name(u), u.id ] },
15
+ @loan.user_id) %>
16
+ </div>
17
+ <div class="hrl-field">
18
+ <%= f.label :principal, "Amount lent (₹)" %>
19
+ <%= f.text_field :principal, value: @loan.principal&.to_s("F"), inputmode: "decimal" %>
20
+ </div>
21
+ <div class="hrl-field">
22
+ <%= f.label :monthly_instalment, "Monthly deduction (₹)" %>
23
+ <%= f.text_field :monthly_instalment, value: @loan.monthly_instalment&.to_s("F"),
24
+ inputmode: "decimal" %>
25
+ </div>
26
+ <div class="hrl-field">
27
+ <%= f.label :starts_on, "First deduction in" %>
28
+ <%= f.date_field :starts_on %>
29
+ </div>
30
+ <div class="hrl-field">
31
+ <%= f.label :reason, "What it is for (optional)" %>
32
+ <%= f.text_field :reason %>
33
+ </div>
34
+ <div class="hrl-form-actions">
35
+ <%= f.submit "Record", class: "hrl-btn hrl-btn--primary" %>
36
+ <a class="hrl-btn" href="<%= hr_lite.admin_loans_path %>">Cancel</a>
37
+ </div>
38
+ <% end %>
39
+ </section>
@@ -0,0 +1,52 @@
1
+ <% content_for(:page_title) { hr_display_name(@loan.user) } %>
2
+ <div class="hrl-page__head">
3
+ <h1 class="hrl-page__title"><%= hr_display_name(@loan.user) %> — <%= hrl_money(@loan.principal) %></h1>
4
+ </div>
5
+
6
+ <section class="hrl-card">
7
+ <div class="hrl-table-wrap">
8
+ <table class="hrl-table hrl-table--stack">
9
+ <tbody>
10
+ <tr><td data-label="Status"><%= @loan.status.humanize %></td></tr>
11
+ <tr><td data-label="Monthly instalment"><%= hrl_money(@loan.monthly_instalment) %></td></tr>
12
+ <tr><td data-label="Starts"><%= @loan.starts_on.strftime("%b %Y") %></td></tr>
13
+ <tr><td data-label="Repaid"><%= hrl_money(@loan.repaid) %></td></tr>
14
+ <tr><td data-label="Outstanding"><strong><%= hrl_money(@loan.outstanding) %></strong></td></tr>
15
+ <% if @loan.reason.present? %><tr><td data-label="Reason"><%= @loan.reason %></td></tr><% end %>
16
+ </tbody>
17
+ </table>
18
+ </div>
19
+
20
+ <% if @loan.active? %>
21
+ <div class="hrl-row">
22
+ <%= button_to "Close (stop deductions)", hr_lite.close_admin_loan_path(@loan),
23
+ method: :post, class: "hrl-btn",
24
+ form: { data: { turbo_confirm: "Stop deducting? The outstanding balance stays on record." } } %>
25
+ <%= button_to "Cancel", hr_lite.cancel_admin_loan_path(@loan),
26
+ method: :post, class: "hrl-btn hrl-btn--danger",
27
+ form: { data: { turbo_confirm: "Cancel this loan entirely?" } } %>
28
+ </div>
29
+ <% end %>
30
+ </section>
31
+
32
+ <section class="hrl-card">
33
+ <h2 class="hrl-card__title">Deducted so far</h2>
34
+ <% repayments = @loan.loan_repayments.sort_by(&:period_month).reverse %>
35
+ <% if repayments.empty? %>
36
+ <p class="hrl-muted">Nothing deducted yet.</p>
37
+ <% else %>
38
+ <div class="hrl-table-wrap">
39
+ <table class="hrl-table hrl-table--stack">
40
+ <thead><tr><th>Month</th><th>Amount</th></tr></thead>
41
+ <tbody>
42
+ <% repayments.each do |r| %>
43
+ <tr>
44
+ <td data-label="Month"><%= r.period_month.strftime("%b %Y") %></td>
45
+ <td data-label="Amount" class="hrl-num"><%= hrl_money(r.amount) %></td>
46
+ </tr>
47
+ <% end %>
48
+ </tbody>
49
+ </table>
50
+ </div>
51
+ <% end %>
52
+ </section>
@@ -0,0 +1,37 @@
1
+ <% content_for(:page_title) { "Tax declarations" } %>
2
+ <div class="hrl-page__head"><h1 class="hrl-page__title">Tax declarations</h1></div>
3
+
4
+ <section class="hrl-card">
5
+ <div class="hrl-row">
6
+ <% HrLite::TaxDeclaration::STATUSES.each do |status| %>
7
+ <a class="hrl-btn <%= "hrl-btn--primary" if @status == status %>"
8
+ href="<%= hr_lite.admin_tax_declarations_path(status: status) %>"><%= status.humanize %></a>
9
+ <% end %>
10
+ </div>
11
+ </section>
12
+
13
+ <section class="hrl-card">
14
+ <% if @declarations.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>Person</th><th>Year</th><th>Regime</th><th>Claimed</th><th></th></tr></thead>
20
+ <tbody>
21
+ <% @declarations.each do |declaration| %>
22
+ <tr>
23
+ <td data-label="Person"><%= hr_display_name(declaration.user) %></td>
24
+ <td data-label="Year"><%= declaration.label %></td>
25
+ <td data-label="Regime"><%= declaration.regime.humanize %></td>
26
+ <td data-label="Claimed" class="hrl-num"><%= hrl_money(declaration.declared_total) %></td>
27
+ <td data-label="">
28
+ <a class="hrl-small" href="<%= hr_lite.admin_tax_declaration_path(declaration) %>">Check</a>
29
+ </td>
30
+ </tr>
31
+ <% end %>
32
+ </tbody>
33
+ </table>
34
+ </div>
35
+ <% end %>
36
+ <%= hrl_pagination %>
37
+ </section>
@@ -0,0 +1,54 @@
1
+ <% content_for(:page_title) { hr_display_name(@declaration.user) } %>
2
+ <div class="hrl-page__head">
3
+ <h1 class="hrl-page__title">
4
+ <%= hr_display_name(@declaration.user) %> — <%= @declaration.label %>
5
+ </h1>
6
+ </div>
7
+
8
+ <section class="hrl-card">
9
+ <p>
10
+ Regime: <strong><%= @declaration.regime.humanize %></strong> ·
11
+ Status: <%= hrl_request_status_badge(@declaration.status == "verified" ? "approved" : @declaration.status) %>
12
+ </p>
13
+ <p class="hrl-muted hrl-small">
14
+ Until this is verified, payroll deducts against what was CLAIMED — making
15
+ somebody overpay tax all year because paperwork is slow is its own kind of
16
+ wrong. Once verified, only the amounts you enter below count.
17
+ </p>
18
+ </section>
19
+
20
+ <%= form_with model: @declaration, url: hr_lite.admin_tax_declaration_path(@declaration),
21
+ method: :patch, scope: :tax_declaration, local: true do |f| %>
22
+ <section class="hrl-card">
23
+ <h2 class="hrl-card__title">What the proof supports</h2>
24
+ <%= f.fields_for :tax_declaration_items do |item| %>
25
+ <div class="hrl-field">
26
+ <%= item.label :verified_amount, item.object.section_label %>
27
+ <span class="hrl-small hrl-muted">
28
+ Claimed <%= hrl_money(item.object.declared_amount) %>
29
+ <%= "— #{item.object.label}" if item.object.label.present? %>
30
+ </span>
31
+ <%= item.text_field :verified_amount,
32
+ value: item.object.verified_amount&.to_s("F"), inputmode: "decimal",
33
+ placeholder: "Verified amount" %>
34
+ </div>
35
+ <% end %>
36
+ <div class="hrl-form-actions">
37
+ <%= f.submit "Record amounts", class: "hrl-btn" %>
38
+ </div>
39
+ </section>
40
+ <% end %>
41
+
42
+ <section class="hrl-card">
43
+ <h2 class="hrl-card__title">Decide</h2>
44
+ <%= button_to "Verify", hr_lite.verify_admin_tax_declaration_path(@declaration),
45
+ method: :post, class: "hrl-btn hrl-btn--primary" %>
46
+ <%= form_with url: hr_lite.reject_admin_tax_declaration_path(@declaration),
47
+ method: :post, local: true do %>
48
+ <div class="hrl-field">
49
+ <%= label_tag :note, "What is missing (required to send back)" %>
50
+ <%= text_field_tag :note %>
51
+ </div>
52
+ <%= submit_tag "Send back", class: "hrl-btn" %>
53
+ <% end %>
54
+ </section>
@@ -0,0 +1,84 @@
1
+ <% content_for(:page_title) { "My documents" } %>
2
+ <div class="hrl-page__head"><h1 class="hrl-page__title">My documents</h1></div>
3
+
4
+ <% if @expiring.any? %>
5
+ <section class="hrl-card hrl-card--warn">
6
+ <h2 class="hrl-card__title">Expiring soon</h2>
7
+ <ul>
8
+ <% @expiring.each do |doc| %>
9
+ <li><%= doc.title %> — <%= doc.expired? ? "expired" : "expires" %>
10
+ <%= doc.expires_on.strftime("%d %b %Y") %></li>
11
+ <% end %>
12
+ </ul>
13
+ </section>
14
+ <% end %>
15
+
16
+ <section class="hrl-card">
17
+ <h2 class="hrl-card__title">Upload a document</h2>
18
+ <%= form_with model: @document, url: hr_lite.documents_path, method: :post, local: true do |f| %>
19
+ <%= render "hr_lite/shared/form_errors", record: @document %>
20
+ <div class="hrl-field">
21
+ <%= f.label :category %>
22
+ <%= f.select :category,
23
+ options_for_select(HrLite::Document::SENSITIVE_CATEGORIES.map { |c| [ c.humanize, c ] } +
24
+ [ [ "Offer letter", "offer_letter" ], [ "Certificate", "certificate" ],
25
+ [ "Other", "other" ] ], @document.category) %>
26
+ </div>
27
+ <div class="hrl-field"><%= f.label :title %><%= f.text_field :title %></div>
28
+ <div class="hrl-field">
29
+ <%= f.label :reference_number, "Document number (optional)" %>
30
+ <%= f.text_field :reference_number %>
31
+ </div>
32
+ <div class="hrl-field">
33
+ <%= f.label :expires_on, "Expires on (optional — we will remind you)" %>
34
+ <%= f.date_field :expires_on %>
35
+ </div>
36
+ <div class="hrl-field">
37
+ <%= f.label :file, "File (PDF or image, up to 10 MB)" %>
38
+ <%= f.file_field :file %>
39
+ </div>
40
+ <div class="hrl-form-actions"><%= f.submit "Upload", class: "hrl-btn hrl-btn--primary" %></div>
41
+ <% end %>
42
+ </section>
43
+
44
+ <section class="hrl-card">
45
+ <h2 class="hrl-card__title">On file</h2>
46
+ <% if @documents.empty? %>
47
+ <p class="hrl-muted">Nothing uploaded yet.</p>
48
+ <% else %>
49
+ <div class="hrl-table-wrap">
50
+ <table class="hrl-table hrl-table--stack">
51
+ <thead><tr><th>Document</th><th>Expires</th><th>Checked</th><th></th></tr></thead>
52
+ <tbody>
53
+ <% @documents.each do |doc| %>
54
+ <tr>
55
+ <td data-label="Document">
56
+ <strong><%= doc.title %></strong>
57
+ <div class="hrl-small hrl-muted"><%= doc.category.humanize %></div>
58
+ </td>
59
+ <td data-label="Expires">
60
+ <%= doc.expires_on ? doc.expires_on.strftime("%d %b %Y") : "—" %>
61
+ </td>
62
+ <td data-label="Checked">
63
+ <%= hrl_request_status_badge(doc.verification == "verified" ? "approved" : doc.verification) %>
64
+ <% if doc.verification_note.present? %>
65
+ <div class="hrl-small hrl-muted"><%= doc.verification_note %></div>
66
+ <% end %>
67
+ </td>
68
+ <td data-label="">
69
+ <% if doc.file.attached? %>
70
+ <a class="hrl-small" href="<%= hr_lite.download_document_path(doc) %>">Open</a>
71
+ <% end %>
72
+ <% unless doc.verified? %>
73
+ <%= button_to "Remove", hr_lite.document_path(doc), method: :delete,
74
+ class: "hrl-btn",
75
+ form: { data: { turbo_confirm: "Remove #{doc.title}?" } } %>
76
+ <% end %>
77
+ </td>
78
+ </tr>
79
+ <% end %>
80
+ </tbody>
81
+ </table>
82
+ </div>
83
+ <% end %>
84
+ </section>
@@ -0,0 +1,52 @@
1
+ <% content_for(:page_title) { "My loans" } %>
2
+ <div class="hrl-page__head"><h1 class="hrl-page__title">Loans &amp; advances</h1></div>
3
+
4
+ <% if @loans.empty? %>
5
+ <section class="hrl-card"><p class="hrl-muted">You have no loans or salary advances.</p></section>
6
+ <% else %>
7
+ <section class="hrl-card">
8
+ <p>Outstanding across everything: <strong><%= hrl_money(@outstanding) %></strong></p>
9
+ </section>
10
+
11
+ <% @loans.each do |loan| %>
12
+ <section class="hrl-card">
13
+ <h2 class="hrl-card__title">
14
+ <%= hrl_money(loan.principal) %>
15
+ <span class="hrl-badge <%= "hrl-badge--muted" unless loan.active? %>"><%= loan.status.humanize %></span>
16
+ </h2>
17
+ <% if loan.reason.present? %><p class="hrl-muted"><%= loan.reason %></p><% end %>
18
+ <div class="hrl-table-wrap">
19
+ <table class="hrl-table hrl-table--stack">
20
+ <tbody>
21
+ <tr><td data-label="Monthly instalment"><%= hrl_money(loan.monthly_instalment) %></td></tr>
22
+ <tr><td data-label="Repaid so far"><%= hrl_money(loan.repaid) %></td></tr>
23
+ <tr><td data-label="Outstanding"><strong><%= hrl_money(loan.outstanding) %></strong></td></tr>
24
+ <tr>
25
+ <td data-label="Next deduction">
26
+ <% amount = loan.instalment_for(@next_month) %>
27
+ <%= amount.positive? ? "#{hrl_money(amount)} in #{@next_month.strftime('%b %Y')}" : "—" %>
28
+ </td>
29
+ </tr>
30
+ </tbody>
31
+ </table>
32
+ </div>
33
+
34
+ <% if loan.loan_repayments.any? %>
35
+ <h3 class="hrl-card__title">Already deducted</h3>
36
+ <div class="hrl-table-wrap">
37
+ <table class="hrl-table hrl-table--stack">
38
+ <thead><tr><th>Month</th><th>Amount</th></tr></thead>
39
+ <tbody>
40
+ <% loan.loan_repayments.sort_by(&:period_month).reverse.each do |r| %>
41
+ <tr>
42
+ <td data-label="Month"><%= r.period_month.strftime("%b %Y") %></td>
43
+ <td data-label="Amount" class="hrl-num"><%= hrl_money(r.amount) %></td>
44
+ </tr>
45
+ <% end %>
46
+ </tbody>
47
+ </table>
48
+ </div>
49
+ <% end %>
50
+ </section>
51
+ <% end %>
52
+ <% end %>
@@ -0,0 +1,68 @@
1
+ <% content_for(:page_title) { "Tax declaration" } %>
2
+ <div class="hrl-page__head">
3
+ <h1 class="hrl-page__title">Tax declaration — <%= @declaration.label %></h1>
4
+ </div>
5
+
6
+ <section class="hrl-card">
7
+ <p class="hrl-muted hrl-small">
8
+ Your whole year's TDS is projected from these numbers, so a declaration
9
+ filed in April spreads the tax evenly instead of landing it in March.
10
+ Claim what you intend to invest; HR records what the proof supports when
11
+ you hand it over.
12
+ </p>
13
+ <p>
14
+ Status: <%= hrl_request_status_badge(@declaration.status == "verified" ? "approved" : @declaration.status) %>
15
+ <% if @declaration.note.present? %>
16
+ <span class="hrl-small hrl-muted">— <%= @declaration.note %></span>
17
+ <% end %>
18
+ </p>
19
+ </section>
20
+
21
+ <%= form_with model: @declaration, url: hr_lite.tax_declaration_path, method: :patch,
22
+ scope: :tax_declaration, local: true do |f| %>
23
+ <%= render "hr_lite/shared/form_errors", record: @declaration %>
24
+
25
+ <section class="hrl-card">
26
+ <div class="hrl-field">
27
+ <%= f.label :regime, "Tax regime" %>
28
+ <%= f.select :regime, [ [ "New regime (lower rates, no deductions)", "new" ],
29
+ [ "Old regime (deductions apply)", "old" ] ] %>
30
+ <span class="hrl-small hrl-muted">
31
+ The deductions below only reduce tax under the old regime.
32
+ </span>
33
+ </div>
34
+ </section>
35
+
36
+ <section class="hrl-card">
37
+ <h2 class="hrl-card__title">What you are claiming</h2>
38
+ <%= f.fields_for :tax_declaration_items do |item| %>
39
+ <div class="hrl-field">
40
+ <%= item.hidden_field :section %>
41
+ <%= item.label :declared_amount, item.object.section_label %>
42
+ <%= item.text_field :declared_amount,
43
+ value: item.object.declared_amount&.to_s("F"), inputmode: "decimal" %>
44
+ <%= item.text_field :label, placeholder: "What it is (optional)" %>
45
+ <% if item.object.persisted? && item.object.verified_amount %>
46
+ <span class="hrl-small hrl-muted">
47
+ HR verified <%= hrl_money(item.object.verified_amount) %>
48
+ </span>
49
+ <% end %>
50
+ </div>
51
+ <% end %>
52
+ <p>Total claimed: <strong><%= hrl_money(@declaration.declared_total) %></strong></p>
53
+ <div class="hrl-form-actions">
54
+ <%= f.submit "Save draft", class: "hrl-btn hrl-btn--primary" %>
55
+ </div>
56
+ </section>
57
+ <% end %>
58
+
59
+ <% if @declaration.persisted? && (@declaration.draft? || @declaration.rejected?) %>
60
+ <section class="hrl-card">
61
+ <p class="hrl-muted hrl-small">
62
+ Submitting locks it for HR to check. They can send it back if the proof
63
+ does not match.
64
+ </p>
65
+ <%= button_to "Submit to HR", hr_lite.submit_tax_declaration_path,
66
+ method: :post, class: "hrl-btn hrl-btn--primary" %>
67
+ </section>
68
+ <% end %>
data/config/routes.rb CHANGED
@@ -30,6 +30,13 @@ HrLite::Engine.routes.draw do
30
30
  member { post :acknowledge }
31
31
  end
32
32
  resources :benefits, only: :index
33
+ resources :documents, only: %i[index create destroy] do
34
+ member { get :download }
35
+ end
36
+ resource :tax_declaration, only: %i[show update], controller: "tax_declarations" do
37
+ post :submit
38
+ end
39
+ resources :loans, only: :index
33
40
  get "team", to: "team#show"
34
41
  get "org", to: "org#show"
35
42
  resources :holidays, only: :index
@@ -61,7 +68,29 @@ HrLite::Engine.routes.draw do
61
68
  resources :assignments, only: %i[create destroy], controller: "role_assignments"
62
69
  end
63
70
  resources :statutory_rate_cards, only: %i[index new create edit update]
71
+ resources :documents, only: :index do
72
+ member { post :verify; post :reject }
73
+ end
74
+ get "documents/for/:user_id", to: "documents#show", as: :employee_documents
75
+ resources :tax_declarations, only: %i[index show update] do
76
+ member { post :verify; post :reject }
77
+ end
78
+ resources :loans, only: %i[index new create show] do
79
+ member { post :close; post :cancel }
80
+ end
64
81
  resources :reports, only: %i[index show]
82
+ resources :expenses, only: :index do
83
+ member { post :approve; post :reject; post :reimburse }
84
+ end
85
+ resources :hr_requests, only: %i[index show] do
86
+ member { post :assign; post :resolve }
87
+ end
88
+ resources :assets, only: %i[index new create] do
89
+ member { post :assign; post :take_back }
90
+ end
91
+ resources :checklists, only: :index do
92
+ member { post :complete; post :reopen }
93
+ end
65
94
  resources :leave_types, except: :show
66
95
  resources :office_locations, except: :show
67
96
  resources :holidays, only: %i[index create update destroy] do
@@ -0,0 +1,76 @@
1
+ class CreateHrLiteAssetsAndOnboarding < ActiveRecord::Migration[8.1]
2
+ # The two ends of employment that were still manual: the laptop somebody is
3
+ # given on day one, and the checklist nobody remembers on their last.
4
+ #
5
+ # An asset that is never returned is a cost nobody notices, and access that
6
+ # is never revoked is a security problem that outlives the employment.
7
+ def change
8
+ create_table :hr_lite_assets do |t|
9
+ t.string :name, null: false
10
+ # laptop | phone | sim | id_card | accessory | vehicle | other
11
+ t.string :category, null: false, default: "other"
12
+ t.string :serial_number
13
+ t.date :purchased_on
14
+ # available | assigned | returned | lost | damaged | retired
15
+ t.string :status, null: false, default: "available"
16
+ t.text :notes
17
+ t.timestamps
18
+ end
19
+ add_index :hr_lite_assets, :status
20
+ add_index :hr_lite_assets, :serial_number, unique: true,
21
+ where: "serial_number IS NOT NULL"
22
+ add_check_constraint :hr_lite_assets,
23
+ "status IN ('available', 'assigned', 'returned', 'lost', 'damaged', 'retired')",
24
+ name: "hr_lite_assets_status_check"
25
+
26
+ # Append-only: who had it, when, and what state it came back in. Editing
27
+ # an assignment away is how a missing laptop stops being anybody's.
28
+ create_table :hr_lite_asset_assignments do |t|
29
+ t.references :asset, null: false, index: true,
30
+ foreign_key: { to_table: :hr_lite_assets, on_delete: :cascade }
31
+ t.bigint :user_id, null: false
32
+ t.date :assigned_on, null: false
33
+ t.date :returned_on
34
+ t.string :condition_note
35
+ t.bigint :assigned_by_id
36
+ t.timestamps
37
+ end
38
+ add_index :hr_lite_asset_assignments, %i[user_id returned_on]
39
+ # One live holder at a time — two open assignments means the asset is in
40
+ # two places, which it is not.
41
+ add_index :hr_lite_asset_assignments, :asset_id,
42
+ unique: true, where: "returned_on IS NULL",
43
+ name: "index_hr_lite_asset_assignments_one_live_per_asset"
44
+
45
+ create_table :hr_lite_checklist_templates do |t|
46
+ # onboarding | offboarding
47
+ t.string :kind, null: false
48
+ t.string :title, null: false
49
+ t.integer :position, null: false, default: 0
50
+ # Which permission the person who ticks it should hold — IT hands back
51
+ # a laptop, HR collects a document, and neither should be the other.
52
+ t.string :owner_permission
53
+ # Days from the joining or leaving date this is due.
54
+ t.integer :due_offset_days, null: false, default: 0
55
+ t.boolean :active, null: false, default: true
56
+ t.timestamps
57
+ end
58
+ add_index :hr_lite_checklist_templates, %i[kind position]
59
+ add_check_constraint :hr_lite_checklist_templates, "kind IN ('onboarding', 'offboarding')",
60
+ name: "hr_lite_checklist_templates_kind_check"
61
+
62
+ create_table :hr_lite_checklist_items do |t|
63
+ t.bigint :user_id, null: false
64
+ t.references :template, index: true,
65
+ foreign_key: { to_table: :hr_lite_checklist_templates, on_delete: :nullify }
66
+ t.string :kind, null: false
67
+ t.string :title, null: false
68
+ t.date :due_on
69
+ t.datetime :completed_at
70
+ t.bigint :completed_by_id
71
+ t.string :note
72
+ t.timestamps
73
+ end
74
+ add_index :hr_lite_checklist_items, %i[user_id kind completed_at]
75
+ end
76
+ end
@@ -56,6 +56,9 @@ module HrLite
56
56
  "hr_request.manage" => [ "Help desk", "Answer and assign HR requests" ],
57
57
  "policy.view" => [ "Policies", "Read published policies" ],
58
58
  "policy.manage" => [ "Policies", "Write, publish and re-issue policies" ],
59
+ "asset.view" => [ "Assets", "See company assets and who holds them" ],
60
+ "asset.manage" => [ "Assets", "Assign and take back company assets" ],
61
+ "checklist.manage" => [ "Lifecycle", "Work through joining and leaving checklists" ],
59
62
  "settings.manage" => [ "Administration", "Change company settings, offices and policy" ],
60
63
  "audit.view" => [ "Administration", "Read the audit trail" ],
61
64
  "audit.view_money" => [ "Administration", "Read audit rows about pay, appraisals and promotions" ],
@@ -19,7 +19,7 @@ module HrLite
19
19
  "profile.view" => "self", "appraisal.view" => "self",
20
20
  "resignation.view" => "self", "document.view" => "self", "tax.view" => "self",
21
21
  "expense.claim" => "self", "benefit.view" => "self",
22
- "hr_request.raise" => "self", "policy.view" => "all"
22
+ "hr_request.raise" => "self", "policy.view" => "all", "asset.view" => "self"
23
23
  }
24
24
  },
25
25
  Role::MANAGER => {
@@ -40,7 +40,8 @@ module HrLite
40
40
  "appraisal.view" => "self", "resignation.view" => "all",
41
41
  "document.view" => "all", "tax.view" => "self",
42
42
  "expense.claim" => "self", "benefit.view" => "all", "benefit.manage" => "all",
43
- "hr_request.raise" => "self", "hr_request.manage" => "all", "policy.view" => "all"
43
+ "hr_request.raise" => "self", "hr_request.manage" => "all", "policy.view" => "all",
44
+ "asset.view" => "all", "asset.manage" => "all", "checklist.manage" => "all"
44
45
  }
45
46
  },
46
47
  Role::FINANCE => {
@@ -65,7 +66,8 @@ module HrLite
65
66
  "resignation.view" => "all", "resignation.manage" => "all",
66
67
  "settings.manage" => "all", "audit.view" => "all", "payroll.view" => "self",
67
68
  "document.view" => "all", "expense.approve" => "all", "benefit.manage" => "all",
68
- "hr_request.manage" => "all", "policy.view" => "all", "policy.manage" => "all"
69
+ "hr_request.manage" => "all", "policy.view" => "all", "policy.manage" => "all",
70
+ "asset.view" => "all", "asset.manage" => "all", "checklist.manage" => "all"
69
71
  }
70
72
  },
71
73
  Role::SUPER_ADMIN => {
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.13.0"
2
+ VERSION = "0.15.0"
3
3
  end