hr_lite 0.14.0 → 0.16.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -1
  3. data/README.md +4 -1
  4. data/app/assets/javascripts/hr_lite/print.js +5 -0
  5. data/app/assets/stylesheets/hr_lite/hr_lite.css +72 -0
  6. data/app/controllers/hr_lite/admin/documents_controller.rb +63 -0
  7. data/app/controllers/hr_lite/admin/employees_controller.rb +1 -1
  8. data/app/controllers/hr_lite/admin/holidays_controller.rb +7 -0
  9. data/app/controllers/hr_lite/admin/leave_requests_controller.rb +23 -0
  10. data/app/controllers/hr_lite/admin/loans_controller.rb +59 -0
  11. data/app/controllers/hr_lite/admin/policies_controller.rb +29 -0
  12. data/app/controllers/hr_lite/admin/tax_declarations_controller.rb +75 -0
  13. data/app/controllers/hr_lite/documents_controller.rb +69 -0
  14. data/app/controllers/hr_lite/hr_requests_controller.rb +2 -0
  15. data/app/controllers/hr_lite/id_cards_controller.rb +25 -0
  16. data/app/controllers/hr_lite/loans_controller.rb +11 -0
  17. data/app/controllers/hr_lite/tax_declarations_controller.rb +100 -0
  18. data/app/helpers/hr_lite/application_helper.rb +18 -3
  19. data/app/models/hr_lite/approval_flow.rb +1 -1
  20. data/app/models/hr_lite/employee_profile.rb +21 -3
  21. data/app/models/hr_lite/hr_request.rb +12 -3
  22. data/app/models/hr_lite/leave_request.rb +15 -4
  23. data/app/models/hr_lite/policy.rb +15 -0
  24. data/app/models/hr_lite/setting.rb +1 -1
  25. data/app/models/hr_lite/tax_declaration.rb +4 -1
  26. data/app/views/hr_lite/admin/documents/index.html.erb +89 -0
  27. data/app/views/hr_lite/admin/documents/show.html.erb +42 -0
  28. data/app/views/hr_lite/admin/employees/_form.html.erb +5 -0
  29. data/app/views/hr_lite/admin/employees/show.html.erb +1 -0
  30. data/app/views/hr_lite/admin/hr_requests/show.html.erb +3 -0
  31. data/app/views/hr_lite/admin/leave_requests/index.html.erb +3 -0
  32. data/app/views/hr_lite/admin/leave_requests/new.html.erb +35 -0
  33. data/app/views/hr_lite/admin/loans/index.html.erb +43 -0
  34. data/app/views/hr_lite/admin/loans/new.html.erb +39 -0
  35. data/app/views/hr_lite/admin/loans/show.html.erb +52 -0
  36. data/app/views/hr_lite/admin/policies/index.html.erb +27 -0
  37. data/app/views/hr_lite/admin/policies/new.html.erb +22 -0
  38. data/app/views/hr_lite/admin/tax_declarations/index.html.erb +37 -0
  39. data/app/views/hr_lite/admin/tax_declarations/show.html.erb +54 -0
  40. data/app/views/hr_lite/documents/index.html.erb +84 -0
  41. data/app/views/hr_lite/id_cards/show.html.erb +86 -0
  42. data/app/views/hr_lite/loans/index.html.erb +52 -0
  43. data/app/views/hr_lite/shared/_bell.html.erb +3 -0
  44. data/app/views/hr_lite/tax_declarations/show.html.erb +68 -0
  45. data/app/views/layouts/hr_lite/application.html.erb +2 -0
  46. data/config/routes.rb +21 -1
  47. data/db/migrate/20260927100145_add_id_card_fields_to_hr_lite_employee_profiles.rb +6 -0
  48. data/db/migrate/20260927101031_add_created_by_id_to_hr_lite_leave_requests.rb +5 -0
  49. data/lib/hr_lite/configuration.rb +4 -1
  50. data/lib/hr_lite/notifications.rb +4 -3
  51. data/lib/hr_lite/version.rb +1 -1
  52. data/lib/hr_lite.rb +1 -0
  53. metadata +41 -1
@@ -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,27 @@
1
+ <% content_for(:page_title) { "Policies and announcements" } %>
2
+ <div class="hrl-page__head">
3
+ <h1 class="hrl-page__title">Policies and announcements</h1>
4
+ <div class="hrl-page__actions"><a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_admin_policy_path %>">Publish</a></div>
5
+ </div>
6
+
7
+ <section class="hrl-card">
8
+ <% if @policies.empty? %>
9
+ <p class="hrl-muted">Nothing published yet.</p>
10
+ <% else %>
11
+ <div class="hrl-table-wrap">
12
+ <table class="hrl-table hrl-table--stack">
13
+ <thead><tr><th>Title</th><th>Kind</th><th>In force</th><th>Still to acknowledge</th></tr></thead>
14
+ <tbody>
15
+ <% @policies.each do |policy| %>
16
+ <tr>
17
+ <td data-label="Title"><%= policy.title %> <span class="hrl-badge hrl-badge--muted">v<%= policy.version %></span></td>
18
+ <td data-label="Kind"><%= policy.announcement? ? "Announcement" : "Policy" %></td>
19
+ <td data-label="In force"><%= policy.effective_from.strftime("%d %b %Y") %></td>
20
+ <td data-label="Still to acknowledge"><%= policy.announcement? ? "—" : policy.outstanding_for.count %></td>
21
+ </tr>
22
+ <% end %>
23
+ </tbody>
24
+ </table>
25
+ </div>
26
+ <% end %>
27
+ </section>
@@ -0,0 +1,22 @@
1
+ <% content_for(:page_title) { "Publish" } %>
2
+ <div class="hrl-page__head">
3
+ <h1 class="hrl-page__title">Publish a policy or announcement</h1>
4
+ <p class="hrl-page__sub">Every employee gets an email and a notification. Reusing a title publishes the next version and asks everyone again.</p>
5
+ </div>
6
+
7
+ <section class="hrl-card">
8
+ <%= form_with model: @policy, url: hr_lite.admin_policies_path, method: :post, local: true do |f| %>
9
+ <%= render "hr_lite/shared/form_errors", record: @policy %>
10
+ <div class="hrl-field"><%= f.label :title %><%= f.text_field :title, required: true %></div>
11
+ <div class="hrl-field"><%= f.label :body, "Text" %><%= f.text_area :body, rows: 10, required: true %></div>
12
+ <div class="hrl-field"><%= f.label :effective_from, "In force from" %><%= f.date_field :effective_from %></div>
13
+ <div class="hrl-field hrl-field--check">
14
+ <%= f.check_box :acknowledgement_required %>
15
+ <%= f.label :acknowledgement_required, "Policy — everyone must acknowledge it (untick for an announcement)" %>
16
+ </div>
17
+ <div class="hrl-form-actions">
18
+ <%= f.submit "Publish", class: "hrl-btn hrl-btn--primary" %>
19
+ <a class="hrl-btn" href="<%= hr_lite.admin_policies_path %>">Cancel</a>
20
+ </div>
21
+ <% end %>
22
+ </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,86 @@
1
+ <% content_for(:page_title) { "ID card" } %>
2
+ <% company = HrLite.config.company.call %>
3
+ <% brand = company[:brand].presence || company[:name].presence || "HR" %>
4
+ <% own = @user == hr_current_user %>
5
+ <div class="hrl-page__head">
6
+ <h1 class="hrl-page__title"><%= own ? "My ID card" : "ID card — #{hr_display_name(@user)}" %></h1>
7
+ </div>
8
+
9
+ <% if @profile.nil? %>
10
+ <p class="hrl-card hrl-card--empty"><%= own ? "Your HR profile has not been set up yet, so there is no card to show." : "No HR profile yet." %></p>
11
+ <% else %>
12
+ <% first, rest = hr_display_name(@user).to_s.split(" ", 2) %>
13
+ <div class="hrl-idcard-faces">
14
+ <div class="hrl-idcard-face">
15
+ <span class="hrl-idcard__label">FRONT</span>
16
+ <div class="hrl-idcard">
17
+ <div class="hrl-idcard__band">
18
+ <span class="hrl-idcard__slot"></span>
19
+ <span class="hrl-idcard__logo">
20
+ <% if company[:logo_url].present? %><img src="<%= company[:logo_url] %>" alt="<%= brand %>"><% else %><%= brand %><% end %>
21
+ </span>
22
+ <span class="hrl-idcard__kicker">EMPLOYEE IDENTITY CARD</span>
23
+ </div>
24
+ <div class="hrl-idcard__photo">
25
+ <% if @profile.photo.attached? %>
26
+ <img src="<%= Rails.application.routes.url_helpers.rails_blob_path(@profile.photo, disposition: "inline", only_path: true) %>" alt="<%= hr_display_name(@user) %>">
27
+ <% else %>
28
+ <span><%= hr_display_name(@user).to_s.scan(/\b[[:alpha:]]/).first(2).join.upcase %></span>
29
+ <% end %>
30
+ </div>
31
+ <div class="hrl-idcard__who">
32
+ <div class="hrl-idcard__name"><span class="hrl-idcard__green"><%= first %></span> <span class="hrl-idcard__blue"><%= rest %></span></div>
33
+ <div class="hrl-idcard__role"><%= @profile.designation %></div>
34
+ </div>
35
+ <dl class="hrl-idcard__facts">
36
+ <div><dt>EMPLOYEE ID</dt><dd><%= @profile.employee_code %></dd></div>
37
+ <div><dt>DEPARTMENT</dt><dd><%= @profile.department.presence || "—" %></dd></div>
38
+ <div><dt>BLOOD GROUP</dt><dd class="hrl-idcard__red"><%= @profile.blood_group.presence || "—" %></dd></div>
39
+ <div><dt>JOINED</dt><dd><%= @profile.date_of_joining.strftime("%b %Y") %></dd></div>
40
+ </dl>
41
+ <div class="hrl-idcard__strip"><span></span><span></span><span></span></div>
42
+ </div>
43
+ </div>
44
+
45
+ <div class="hrl-idcard-face">
46
+ <span class="hrl-idcard__label">BACK</span>
47
+ <div class="hrl-idcard hrl-idcard--back">
48
+ <span class="hrl-idcard__slot hrl-idcard__slot--back"></span>
49
+ <div class="hrl-idcard__title"><span class="hrl-idcard__green">Scan to</span> <span class="hrl-idcard__blue">Verify</span></div>
50
+ <div class="hrl-idcard__qr"><%= hrl_office_qr_svg(company, brand) %></div>
51
+ <div class="hrl-idcard__code"><%= @profile.employee_code %></div>
52
+ <div class="hrl-idcard__rows">
53
+ <div><span>Emergency contact</span><b><%= @profile.emergency_contact.presence || "—" %></b></div>
54
+ <hr>
55
+ <div><span>Issued</span><b><%= @issued_on&.strftime("%b %Y") || "Not yet issued" %></b></div>
56
+ </div>
57
+ <p class="hrl-idcard__note">This card is the property of <%= brand %>. If found, please return it to our office<%= " or call #{company[:phone]}" if company[:phone].present? %>.</p>
58
+ <div class="hrl-idcard__foot"><b><%= brand %></b><span>AUTHORISED SIGNATORY</span></div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+
63
+ <div class="hrl-idcard-actions">
64
+ <button type="button" class="hrl-btn hrl-btn--primary" data-hrl-print>Print</button>
65
+ <% if own && @print_requested %>
66
+ <p class="hrl-small hrl-muted">Printed card requested on <%= @print_requested.to_date.strftime("%d %b %Y") %>.</p>
67
+ <% elsif own %>
68
+ <%= button_to "Request printed card", hr_lite.hr_requests_path, class: "hrl-btn",
69
+ params: { hr_request: { category: "id_card", subject: "Please print my ID card" } } %>
70
+ <% end %>
71
+ <% if own %>
72
+ <%= form_with url: hr_lite.id_card_photo_path, method: :patch, multipart: true, class: "hrl-row" do |f| %>
73
+ <div class="hrl-field">
74
+ <label for="photo">Change photo</label>
75
+ <%= f.file_field :photo, id: "photo", accept: HrLite::EmployeeProfile::PHOTO_TYPES.join(","), required: true %>
76
+ <p class="hrl-hint">Face centred, plain background. JPG, PNG or WebP, under <%= HrLite::EmployeeProfile::PHOTO_MAX_BYTES / 1.megabyte %> MB.</p>
77
+ </div>
78
+ <%= f.submit "Upload photo", class: "hrl-btn" %>
79
+ <% end %>
80
+ <% end %>
81
+ </div>
82
+ <% end %>
83
+
84
+ <% content_for :scripts do %>
85
+ <%= javascript_include_tag "hr_lite/print", defer: true %>
86
+ <% end %>
@@ -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,3 @@
1
+ <% if (bell = HrLite.config.notifications&.call(hr_current_user)) %>
2
+ <a class="hrl-bell" href="<%= bell[:url] %>">Notifications<% if bell[:unread].to_i.positive? %> <span class="hrl-bell__count"><%= bell[:unread] %></span><% end %></a>
3
+ <% 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 %>
@@ -56,6 +56,7 @@
56
56
 
57
57
  <div class="hrl-side__foot">
58
58
  <span class="hrl-side__user" title="<%= hr_current_user&.email %>"><%= hr_display_name(hr_current_user) %></span>
59
+ <%= render "hr_lite/shared/bell" %>
59
60
  <% if HrLite.config.back_link %>
60
61
  <a class="hrl-side__back" href="<%= HrLite.config.back_link[:url] %>"><%= HrLite.config.back_link[:label] %></a>
61
62
  <% end %>
@@ -71,6 +72,7 @@
71
72
  <%= company[:name].presence || "HR" %>
72
73
  <% end %>
73
74
  </a>
75
+ <%= render "hr_lite/shared/bell" %>
74
76
  <span class="hrl-appbar__user"><%= hr_display_name(hr_current_user) %></span>
75
77
  </header>
76
78
 
data/config/routes.rb CHANGED
@@ -30,12 +30,21 @@ 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
36
43
  get "calendar", to: "calendar#show"
37
44
  resources :salary_slips, only: %i[index show]
38
45
  resource :employee_profile, only: :show, path: "profile"
46
+ patch "id_card/photo", to: "id_cards#photo", as: :id_card_photo
47
+ get "id_card(/:user_id)", to: "id_cards#show", as: :id_card
39
48
  resource :resignation, only: %i[show create], controller: "resignations" do
40
49
  post :withdraw
41
50
  end
@@ -43,9 +52,10 @@ HrLite::Engine.routes.draw do
43
52
  resources :appraisals, only: %i[index show]
44
53
 
45
54
  namespace :admin do
55
+ resources :policies, only: %i[index new create]
46
56
  get "overview", to: "overview#index"
47
57
  resources :attendances, only: %i[index show update], param: :user_id
48
- resources :leave_requests, only: %i[index show] do
58
+ resources :leave_requests, only: %i[index show new create] do
49
59
  member { post :approve; post :reject; post :cancel }
50
60
  end
51
61
  resources :leave_balances, only: :index do
@@ -61,6 +71,16 @@ HrLite::Engine.routes.draw do
61
71
  resources :assignments, only: %i[create destroy], controller: "role_assignments"
62
72
  end
63
73
  resources :statutory_rate_cards, only: %i[index new create edit update]
74
+ resources :documents, only: :index do
75
+ member { post :verify; post :reject }
76
+ end
77
+ get "documents/for/:user_id", to: "documents#show", as: :employee_documents
78
+ resources :tax_declarations, only: %i[index show update] do
79
+ member { post :verify; post :reject }
80
+ end
81
+ resources :loans, only: %i[index new create show] do
82
+ member { post :close; post :cancel }
83
+ end
64
84
  resources :reports, only: %i[index show]
65
85
  resources :expenses, only: :index do
66
86
  member { post :approve; post :reject; post :reimburse }
@@ -0,0 +1,6 @@
1
+ class AddIdCardFieldsToHrLiteEmployeeProfiles < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :hr_lite_employee_profiles, :blood_group, :text
4
+ add_column :hr_lite_employee_profiles, :emergency_contact, :text
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class AddCreatedByIdToHrLiteLeaveRequests < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :hr_lite_leave_requests, :created_by_id, :integer, limit: 8
4
+ end
5
+ end
@@ -9,7 +9,10 @@ module HrLite
9
9
  :time_zone, :currency_symbol, :on_designation_change,
10
10
  :leadership_emails, :extra_stylesheets, :superadmin_emails,
11
11
  :mailer_from, :public_url_base, :notification_matrix, :back_link,
12
- :onboard_user, :offboard_user, :invite_url_for
12
+ :onboard_user, :offboard_user, :invite_url_for,
13
+ # ->(user) { { url:, unread: } } — shows a notifications link in
14
+ # the HR shell; nil (default) shows nothing.
15
+ :notifications
13
16
 
14
17
  attr_reader :leave_year_start_month
15
18