hr_lite 0.14.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '04966d2c7a57a06e49c64c6a375b148c497f83340a79445eb0f243dd783246f0'
4
- data.tar.gz: 1112874bf22548910a651e53123056fd1341e045598060660c12f93b284789bb
3
+ metadata.gz: ec0aa9dccb203487f12a0e30cbbae7567da32b9589af4046fe86795c9a3cb5a4
4
+ data.tar.gz: 0e5b02a62ea2925115f71279dd9772f12a103b6cc17fb6d192f0225a3c95504e
5
5
  SHA512:
6
- metadata.gz: '00219df8c7244175712e7a87768f50301fa2f8b0110ae201f343409064bdb1083cce3eef587ad046cf7e1831a1981eb7d3c6e816e2a9d431cbbb86aaf5ef8c89'
7
- data.tar.gz: 53af8da6feddf2be67301f9754f2912a08fe246bada0b68eb89b2eb49b70a3279db4ded5645f3438bc859e1a57be3cde29e6c1fc59526db2747d1cc98a5525c6
6
+ metadata.gz: e1877020700a05db3f7a5a4a715be0c1e8e7f4ad447d3c42eaf6d17b60e0252ef68968eb58f431e7373e43d3452bae468d6e211eac64c3f4e9ca71703920d412
7
+ data.tar.gz: 28d5a185fd3af1373efb3b2db1e3d53d8b64e66ec676a7cfed01c15ffbb8ed2a07b0a921d4351ac6afca41db18e683ae9ad984e99b5ffbf6eeac32209e108af0
data/CHANGELOG.md CHANGED
@@ -7,6 +7,58 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.15.0] - 2026-08-18
11
+
12
+ The screens for three features that shipped as tables nobody could reach. No
13
+ migration — the data layer was already there.
14
+
15
+ ### Fixed
16
+
17
+ - **Documents, tax declarations and loans had no UI at all.** 0.13.0 and
18
+ 0.14.0 shipped their models, tables, payroll integration and an expiry job,
19
+ and every one of them passed its specs — but there was no controller, no
20
+ route and no view. An employee could not upload a passport, HR could not
21
+ verify a PAN, and nobody could file an 80C declaration. Loans at least
22
+ reached a payslip; the other two were unreachable rows.
23
+ - **A tax declaration 500ed when a real browser saved it.** The form posts a
24
+ line for every section, because that is what a list of claimable things
25
+ looks like, and the blank ones violated a NOT NULL amount. The specs had
26
+ only ever posted the lines they filled in. Blank sections are now dropped,
27
+ and clearing an existing amount withdraws that claim.
28
+ - `TaxDeclaration` and `ApprovalFlow` were missing `inverse_of` on their
29
+ nested associations. The foreign key is not derived from the class name, so
30
+ a nested item on an unsaved parent could not see it — every first save
31
+ failed with "declaration must exist".
32
+
33
+ ### Added
34
+
35
+ - **Documents.** Employees upload, see what is expiring and remove anything
36
+ not yet verified; HR sees what is waiting, whose file is missing which
37
+ category, and verifies or sends one back with a reason. Downloads go
38
+ through the row's own `readable_by?` and every one is audited — a file
39
+ leaving the building is the event worth recording.
40
+ - **Tax declarations.** A section-by-section claim the employee files
41
+ themselves, replacing an admin typing one lump sum into the profile. HR
42
+ records what the proof actually supported, which is often less than the
43
+ claim. Until it is verified payroll deducts against the CLAIM: making
44
+ somebody overpay tax all year because paperwork is slow is its own kind of
45
+ wrong.
46
+ - **Loans.** Employees see what they owe and what comes out of the next
47
+ payslip; the money tier records a loan, closes it, or cancels one that has
48
+ not been deducted from yet. Cancelling after an instalment has been taken
49
+ is refused — that money has already left somebody's salary.
50
+ - Nav entries for all three, employee and admin.
51
+
52
+ ### Changed
53
+
54
+ - Verifying a document requires being able to OPEN it. Attesting to a scan
55
+ you are not allowed to look at is not verification, and it keeps an
56
+ Aadhaar with the money tier rather than with whoever happens to run HR.
57
+ - The spec suite deletes its sqlite file before every run. The role and
58
+ rate-card migrations SEED rows, and seeds never overwrite what exists —
59
+ right in production, wrong in a test database, where a leftover file kept
60
+ yesterday's definitions and a change to `RoleSeeds` silently did not apply.
61
+
10
62
  ## [0.14.0] - 2026-08-18
11
63
 
12
64
  The admin half, plus the two ends of employment that were still manual.
@@ -793,7 +845,8 @@ Initial release.
793
845
  bus with per-event channel matrix (bell, email, leadership email/bell),
794
846
  daily leadership digest and an append-only audit trail.
795
847
 
796
- [Unreleased]: https://github.com/kshtzkr/hr_lite/compare/v0.14.0...HEAD
848
+ [Unreleased]: https://github.com/kshtzkr/hr_lite/compare/v0.15.0...HEAD
849
+ [0.15.0]: https://github.com/kshtzkr/hr_lite/compare/v0.14.0...v0.15.0
797
850
  [0.14.0]: https://github.com/kshtzkr/hr_lite/compare/v0.13.0...v0.14.0
798
851
  [0.13.0]: https://github.com/kshtzkr/hr_lite/compare/v0.12.0...v0.13.0
799
852
  [0.12.0]: https://github.com/kshtzkr/hr_lite/compare/v0.11.0...v0.12.0
@@ -0,0 +1,63 @@
1
+ module HrLite
2
+ module Admin
3
+ # HR's side: whose documents are missing, and verifying the ones that
4
+ # arrived. Reaching a person's list is `document.view`; opening the file
5
+ # still goes through the row's own visibility.
6
+ class DocumentsController < BaseController
7
+ skip_before_action :require_operations_access!
8
+ before_action :require_document_access!
9
+
10
+ def index
11
+ @category = params[:category].presence
12
+ scope = hr_scope(Document.includes(:user).recent_first, "document.view")
13
+ scope = scope.where(category: @category) if @category
14
+ @pending = scope.where(verification: "pending").limit(50).to_a
15
+ @documents = paginate(scope)
16
+ @categories = Document.distinct.order(:category).pluck(:category)
17
+ end
18
+
19
+ def show
20
+ @employee = HrLite.user_klass.find(params[:user_id])
21
+ hr_require_reach!("document.view", @employee)
22
+ @documents = Document.for_user(@employee).recent_first
23
+ @missing = Document::SENSITIVE_CATEGORIES - @documents.map(&:category)
24
+ end
25
+
26
+ def verify
27
+ document = find_manageable
28
+ document.verify!(actor: hr_current_user, note: params[:note])
29
+ redirect_back fallback_location: admin_documents_path,
30
+ notice: "#{document.title} verified."
31
+ end
32
+
33
+ def reject
34
+ document = find_manageable
35
+ note = params[:note].to_s.strip
36
+ if note.blank?
37
+ return redirect_back fallback_location: admin_documents_path,
38
+ alert: "Say what is wrong with it — the employee has to re-upload."
39
+ end
40
+
41
+ document.reject!(actor: hr_current_user, note: note)
42
+ redirect_back fallback_location: admin_documents_path, notice: "#{document.title} rejected."
43
+ end
44
+
45
+ private
46
+
47
+ def require_document_access!
48
+ hr_require_permission!("document.view", scope: :team)
49
+ end
50
+
51
+ # You may verify a document you can OPEN. Attesting to a scan you are
52
+ # not allowed to look at is not verification, and the row's visibility
53
+ # is what keeps an Aadhaar with the money tier rather than with
54
+ # whoever happens to run HR.
55
+ def find_manageable
56
+ document = Document.find(params[:id])
57
+ raise ActiveRecord::RecordNotFound unless document.readable_by?(hr_current_user)
58
+
59
+ document
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,59 @@
1
+ module HrLite
2
+ module Admin
3
+ # Loans and salary advances. Money tier: the instalment lands as a
4
+ # deduction on a payslip, so whoever sets it is setting somebody's pay.
5
+ class LoansController < SuperadminController
6
+ def index
7
+ @status = params[:status].presence_in(Loan::STATUSES) || "active"
8
+ @loans = paginate(Loan.includes(:user).where(status: @status).order(starts_on: :desc))
9
+ @outstanding = Loan.active.includes(:loan_repayments).sum(BigDecimal(0), &:outstanding)
10
+ end
11
+
12
+ def new
13
+ @loan = Loan.new(starts_on: Date.current.next_month.beginning_of_month)
14
+ end
15
+
16
+ def create
17
+ @loan = Loan.new(loan_params.merge(approved_by_id: hr_current_user.id))
18
+ if @loan.save
19
+ redirect_to admin_loans_path,
20
+ notice: "Loan recorded for #{HrLite.display_name(@loan.user)} — " \
21
+ "deductions start #{@loan.starts_on.strftime('%b %Y')}."
22
+ else
23
+ render :new, status: :unprocessable_entity
24
+ end
25
+ end
26
+
27
+ def show
28
+ @loan = Loan.includes(:loan_repayments).find(params[:id])
29
+ end
30
+
31
+ # Closing stops the deduction. It does NOT delete the repayments —
32
+ # those are on published payslips and are what the outstanding balance
33
+ # is derived from.
34
+ def close
35
+ loan = Loan.find(params[:id])
36
+ loan.update!(status: "closed")
37
+ redirect_to admin_loans_path, notice: "Closed — no further deductions."
38
+ end
39
+
40
+ def cancel
41
+ loan = Loan.find(params[:id])
42
+ if loan.loan_repayments.exists?
43
+ return redirect_to admin_loan_path(loan),
44
+ alert: "Instalments have already been deducted. Close it instead — " \
45
+ "cancelling would deny money that has left somebody's salary."
46
+ end
47
+
48
+ loan.update!(status: "cancelled")
49
+ redirect_to admin_loans_path, notice: "Cancelled before any deduction."
50
+ end
51
+
52
+ private
53
+
54
+ def loan_params
55
+ params.require(:loan).permit(:user_id, :principal, :monthly_instalment, :starts_on, :reason)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,75 @@
1
+ module HrLite
2
+ module Admin
3
+ # HR checking the proof behind a declaration, line by line. The verified
4
+ # amount is what payroll deducts against once this is done, so it is
5
+ # entered per section rather than accepted wholesale.
6
+ class TaxDeclarationsController < BaseController
7
+ skip_before_action :require_operations_access!
8
+ before_action :require_tax_access!
9
+
10
+ def index
11
+ @status = params[:status].presence_in(TaxDeclaration::STATUSES) || "submitted"
12
+ @declarations = paginate(
13
+ hr_scope(TaxDeclaration.includes(:user, :tax_declaration_items), "tax.view")
14
+ .where(status: @status).order(created_at: :desc)
15
+ )
16
+ end
17
+
18
+ def show
19
+ @declaration = find_visible
20
+ end
21
+
22
+ def update
23
+ @declaration = find_manageable
24
+ # Recording what the proof supported is a separate act from deciding
25
+ # — an amount can be corrected while the decision is still pending.
26
+ if @declaration.update(verification_params)
27
+ redirect_to admin_tax_declaration_path(@declaration), notice: "Amounts recorded."
28
+ else
29
+ render :show, status: :unprocessable_entity
30
+ end
31
+ end
32
+
33
+ def verify
34
+ declaration = find_manageable
35
+ declaration.verify!(actor: hr_current_user, note: params[:note])
36
+ redirect_to admin_tax_declarations_path, notice: "Verified — payroll will use the checked amounts."
37
+ rescue ActiveRecord::RecordInvalid
38
+ redirect_to admin_tax_declaration_path(declaration), alert: "Only a submitted declaration can be verified."
39
+ end
40
+
41
+ def reject
42
+ declaration = find_manageable
43
+ declaration.reject!(actor: hr_current_user, note: params[:note].to_s.strip)
44
+ redirect_to admin_tax_declarations_path, notice: "Sent back to the employee."
45
+ rescue ArgumentError
46
+ redirect_to admin_tax_declaration_path(declaration),
47
+ alert: "Say what is missing — they have to know what to fix."
48
+ end
49
+
50
+ private
51
+
52
+ def require_tax_access!
53
+ hr_require_permission!("tax.view", scope: :all)
54
+ end
55
+
56
+ def find_visible
57
+ declaration = TaxDeclaration.includes(:tax_declaration_items).find(params[:id])
58
+ hr_require_reach!("tax.view", declaration.user)
59
+ declaration
60
+ end
61
+
62
+ def find_manageable
63
+ declaration = TaxDeclaration.find(params[:id])
64
+ hr_require_reach!("tax.manage", declaration.user)
65
+ declaration
66
+ end
67
+
68
+ def verification_params
69
+ params.require(:tax_declaration).permit(
70
+ tax_declaration_items_attributes: %i[id verified_amount]
71
+ )
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,69 @@
1
+ module HrLite
2
+ # An employee's own documents, and the download itself.
3
+ #
4
+ # The FILE is the sensitive part, so every read goes through
5
+ # `Document#readable_by?` rather than through the scope that found the row.
6
+ # A passport is not a payslip: HR can see that one exists without being
7
+ # handed the scan.
8
+ class DocumentsController < ApplicationController
9
+ def index
10
+ @documents = Document.for_user(hr_current_user).recent_first
11
+ @document = Document.new
12
+ @expiring = @documents.select { |d| d.expires_on && d.expires_on <= 60.days.from_now.to_date }
13
+ end
14
+
15
+ def create
16
+ @document = Document.new(document_params)
17
+ @document.user_id = hr_current_user.id
18
+ @document.uploaded_by_id = hr_current_user.id
19
+
20
+ if @document.save
21
+ redirect_to documents_path, notice: "#{@document.title} uploaded."
22
+ else
23
+ @documents = Document.for_user(hr_current_user).recent_first
24
+ @expiring = []
25
+ render :index, status: :unprocessable_entity
26
+ end
27
+ end
28
+
29
+ def destroy
30
+ document = Document.for_user(hr_current_user).find(params[:id])
31
+ # A verified identity document is evidence HR acted on; removing it
32
+ # quietly would leave their verification pointing at nothing.
33
+ if document.verified?
34
+ return redirect_to documents_path, status: :see_other,
35
+ alert: "#{document.title} has been verified — ask HR to replace it."
36
+ end
37
+
38
+ document.destroy!
39
+ redirect_to documents_path, notice: "#{document.title} removed.", status: :see_other
40
+ end
41
+
42
+ # The one place a file leaves the building. Scoped by nothing — the
43
+ # permission check is the row's own, because a document may be readable
44
+ # by its owner, by HR, or by nobody but the money tier.
45
+ def download
46
+ document = Document.find(params[:id])
47
+ unless document.readable_by?(hr_current_user)
48
+ raise ActiveRecord::RecordNotFound
49
+ end
50
+
51
+ AuditLog.record!(
52
+ action: "document.downloaded", subject: document, actor: hr_current_user,
53
+ changes: { "title" => document.title, "category" => document.category,
54
+ "owner" => HrLite.display_name(document.user) }
55
+ )
56
+ # rails_blob_path, not url_for: an engine controller has no default
57
+ # host and url_for(attachment) raises without one.
58
+ redirect_to Rails.application.routes.url_helpers.rails_blob_path(
59
+ document.file, disposition: "attachment", only_path: true
60
+ ), allow_other_host: false
61
+ end
62
+
63
+ private
64
+
65
+ def document_params
66
+ params.require(:document).permit(:category, :title, :reference_number, :issued_on, :expires_on, :file)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,11 @@
1
+ module HrLite
2
+ # What an employee owes and what is coming out of the next payslip. Read
3
+ # only: a loan is agreed with the company, not self-served.
4
+ class LoansController < ApplicationController
5
+ def index
6
+ @loans = Loan.where(user_id: hr_current_user.id).order(starts_on: :desc)
7
+ @outstanding = @loans.sum(BigDecimal(0), &:outstanding)
8
+ @next_month = Date.current.next_month.beginning_of_month
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,100 @@
1
+ module HrLite
2
+ # The employee's own declaration for a financial year. Replaces an admin
3
+ # typing one lump sum into the profile: the whole year's TDS is projected
4
+ # from this, so the person whose money it is fills it in.
5
+ class TaxDeclarationsController < ApplicationController
6
+ def show
7
+ @declaration = current_declaration || build_declaration
8
+ @regime = @declaration.regime
9
+ end
10
+
11
+ def update
12
+ # A bare record, not the form's scaffold: `build_declaration` fills in
13
+ # one blank line per section so the form is a list rather than an empty
14
+ # box, and saving those blanks fails a NOT NULL amount.
15
+ @declaration = current_declaration ||
16
+ TaxDeclaration.new(user_id: hr_current_user.id,
17
+ financial_year: financial_year, regime: employee_regime)
18
+ # A verified declaration is what payroll is deducting against; changing
19
+ # it would silently re-open a decision HR has already made.
20
+ if @declaration.verified?
21
+ return redirect_to tax_declaration_path,
22
+ alert: "This year's declaration is verified. Ask HR to reopen it."
23
+ end
24
+
25
+ if @declaration.update(declaration_params)
26
+ redirect_to tax_declaration_path, notice: "Saved."
27
+ else
28
+ # Re-render needs the full set of lines back, or the form loses the
29
+ # sections the person had not filled in yet.
30
+ fill_missing_sections(@declaration)
31
+ render :show, status: :unprocessable_entity
32
+ end
33
+ end
34
+
35
+ def submit
36
+ @declaration = current_declaration
37
+ return redirect_to tax_declaration_path, alert: "Nothing to submit yet." if @declaration.nil?
38
+
39
+ @declaration.submit!(actor: hr_current_user)
40
+ redirect_to tax_declaration_path, notice: "Submitted — HR will check the proof."
41
+ rescue ActiveRecord::RecordInvalid
42
+ redirect_to tax_declaration_path, alert: "Only a draft can be submitted."
43
+ end
44
+
45
+ private
46
+
47
+ def financial_year
48
+ FinancialYear.start_for(params[:financial_year].present? ? Date.parse(params[:financial_year]) : Date.current)
49
+ rescue Date::Error
50
+ FinancialYear.start_for(Date.current)
51
+ end
52
+
53
+ def current_declaration
54
+ TaxDeclaration.find_by(user_id: hr_current_user.id, financial_year: financial_year)
55
+ end
56
+
57
+ def build_declaration
58
+ declaration = TaxDeclaration.new(user_id: hr_current_user.id, financial_year: financial_year,
59
+ regime: employee_regime)
60
+ # One blank line per section, so the form is a list of the things a
61
+ # person can actually claim rather than an empty box and a plus button.
62
+ fill_missing_sections(declaration)
63
+ declaration
64
+ end
65
+
66
+ # Every section gets a line, whether or not it has been claimed yet.
67
+ def fill_missing_sections(declaration)
68
+ present = declaration.tax_declaration_items.map(&:section)
69
+ (TaxDeclarationItem::SECTIONS - present).each do |section|
70
+ declaration.tax_declaration_items.build(section: section)
71
+ end
72
+ end
73
+
74
+ def employee_regime
75
+ EmployeeProfile.find_by(user_id: hr_current_user.id)&.tax_regime.presence || "new"
76
+ end
77
+
78
+ # The form posts a line for every section, because that is what a list of
79
+ # claimable things looks like. Most of them are blank — a section with no
80
+ # amount is not a claim, and saving it violates a NOT NULL amount.
81
+ # An EXISTING line blanked out is a claim being withdrawn, so it is
82
+ # destroyed rather than skipped.
83
+ def declaration_params
84
+ permitted = params.require(:tax_declaration).permit(
85
+ :regime,
86
+ tax_declaration_items_attributes: %i[id section label declared_amount _destroy]
87
+ )
88
+ items = permitted[:tax_declaration_items_attributes]
89
+ return permitted if items.blank?
90
+
91
+ permitted[:tax_declaration_items_attributes] = items.to_h.filter_map { |key, row|
92
+ next [ key, row.merge("_destroy" => "1") ] if row["declared_amount"].blank? && row["id"].present?
93
+ next if row["declared_amount"].blank?
94
+
95
+ [ key, row ]
96
+ }.to_h
97
+ permitted
98
+ end
99
+ end
100
+ end
@@ -19,7 +19,10 @@ 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 = [
@@ -36,7 +39,8 @@ module HrLite
36
39
  LEADERSHIP_NAV_ITEMS = [
37
40
  { label: "Employees", path: :admin_employees_path, match: [ "/admin/employees" ] },
38
41
  { label: "Settings", path: :admin_leave_types_path, match: [ "/admin/leave_types", "/admin/office_locations", "/admin/holidays", "/admin/setting" ] },
39
- { 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" ] }
40
44
  ].freeze
41
45
 
42
46
  # Money screens, plus role management: granting somebody payroll is the
@@ -45,7 +49,9 @@ module HrLite
45
49
  SUPERADMIN_NAV_ITEMS = [
46
50
  { label: "Payroll", path: :admin_payroll_runs_path, match: [ "/admin/payroll_runs", "/admin/salary_slips" ] },
47
51
  { label: "Roles", path: :admin_roles_path, match: [ "/admin/roles" ] },
48
- { 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" ] }
49
55
  ].freeze
50
56
 
51
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
 
@@ -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,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,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,6 +68,16 @@ 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]
65
82
  resources :expenses, only: :index do
66
83
  member { post :approve; post :reject; post :reimburse }
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.14.0"
2
+ VERSION = "0.15.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hr_lite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kshitiz sinha
@@ -63,6 +63,7 @@ files:
63
63
  - app/controllers/hr_lite/admin/checklists_controller.rb
64
64
  - app/controllers/hr_lite/admin/comp_off_requests_controller.rb
65
65
  - app/controllers/hr_lite/admin/designation_changes_controller.rb
66
+ - app/controllers/hr_lite/admin/documents_controller.rb
66
67
  - app/controllers/hr_lite/admin/employees_controller.rb
67
68
  - app/controllers/hr_lite/admin/expenses_controller.rb
68
69
  - app/controllers/hr_lite/admin/holidays_controller.rb
@@ -71,6 +72,7 @@ files:
71
72
  - app/controllers/hr_lite/admin/leave_balances_controller.rb
72
73
  - app/controllers/hr_lite/admin/leave_requests_controller.rb
73
74
  - app/controllers/hr_lite/admin/leave_types_controller.rb
75
+ - app/controllers/hr_lite/admin/loans_controller.rb
74
76
  - app/controllers/hr_lite/admin/office_locations_controller.rb
75
77
  - app/controllers/hr_lite/admin/overview_controller.rb
76
78
  - app/controllers/hr_lite/admin/payroll_runs_controller.rb
@@ -84,6 +86,7 @@ files:
84
86
  - app/controllers/hr_lite/admin/settings_controller.rb
85
87
  - app/controllers/hr_lite/admin/statutory_rate_cards_controller.rb
86
88
  - app/controllers/hr_lite/admin/superadmin_controller.rb
89
+ - app/controllers/hr_lite/admin/tax_declarations_controller.rb
87
90
  - app/controllers/hr_lite/application_controller.rb
88
91
  - app/controllers/hr_lite/appraisals_controller.rb
89
92
  - app/controllers/hr_lite/approvals_controller.rb
@@ -92,6 +95,7 @@ files:
92
95
  - app/controllers/hr_lite/calendar_controller.rb
93
96
  - app/controllers/hr_lite/career_controller.rb
94
97
  - app/controllers/hr_lite/comp_off_requests_controller.rb
98
+ - app/controllers/hr_lite/documents_controller.rb
95
99
  - app/controllers/hr_lite/employee_profiles_controller.rb
96
100
  - app/controllers/hr_lite/expenses_controller.rb
97
101
  - app/controllers/hr_lite/holidays_controller.rb
@@ -100,11 +104,13 @@ files:
100
104
  - app/controllers/hr_lite/kudos_controller.rb
101
105
  - app/controllers/hr_lite/leave_balances_controller.rb
102
106
  - app/controllers/hr_lite/leave_requests_controller.rb
107
+ - app/controllers/hr_lite/loans_controller.rb
103
108
  - app/controllers/hr_lite/org_controller.rb
104
109
  - app/controllers/hr_lite/policies_controller.rb
105
110
  - app/controllers/hr_lite/regularization_requests_controller.rb
106
111
  - app/controllers/hr_lite/resignations_controller.rb
107
112
  - app/controllers/hr_lite/salary_slips_controller.rb
113
+ - app/controllers/hr_lite/tax_declarations_controller.rb
108
114
  - app/controllers/hr_lite/team_controller.rb
109
115
  - app/controllers/hr_lite/users_controller.rb
110
116
  - app/helpers/hr_lite/application_helper.rb
@@ -195,6 +201,8 @@ files:
195
201
  - app/views/hr_lite/admin/comp_off_requests/index.html.erb
196
202
  - app/views/hr_lite/admin/comp_off_requests/show.html.erb
197
203
  - app/views/hr_lite/admin/designation_changes/new.html.erb
204
+ - app/views/hr_lite/admin/documents/index.html.erb
205
+ - app/views/hr_lite/admin/documents/show.html.erb
198
206
  - app/views/hr_lite/admin/employees/_form.html.erb
199
207
  - app/views/hr_lite/admin/employees/edit.html.erb
200
208
  - app/views/hr_lite/admin/employees/index.html.erb
@@ -211,6 +219,9 @@ files:
211
219
  - app/views/hr_lite/admin/leave_types/edit.html.erb
212
220
  - app/views/hr_lite/admin/leave_types/index.html.erb
213
221
  - app/views/hr_lite/admin/leave_types/new.html.erb
222
+ - app/views/hr_lite/admin/loans/index.html.erb
223
+ - app/views/hr_lite/admin/loans/new.html.erb
224
+ - app/views/hr_lite/admin/loans/show.html.erb
214
225
  - app/views/hr_lite/admin/office_locations/_form.html.erb
215
226
  - app/views/hr_lite/admin/office_locations/edit.html.erb
216
227
  - app/views/hr_lite/admin/office_locations/index.html.erb
@@ -237,6 +248,8 @@ files:
237
248
  - app/views/hr_lite/admin/statutory_rate_cards/edit.html.erb
238
249
  - app/views/hr_lite/admin/statutory_rate_cards/index.html.erb
239
250
  - app/views/hr_lite/admin/statutory_rate_cards/new.html.erb
251
+ - app/views/hr_lite/admin/tax_declarations/index.html.erb
252
+ - app/views/hr_lite/admin/tax_declarations/show.html.erb
240
253
  - app/views/hr_lite/appraisals/index.html.erb
241
254
  - app/views/hr_lite/appraisals/show.html.erb
242
255
  - app/views/hr_lite/approvals/index.html.erb
@@ -248,6 +261,7 @@ files:
248
261
  - app/views/hr_lite/career/show.html.erb
249
262
  - app/views/hr_lite/comp_off_requests/index.html.erb
250
263
  - app/views/hr_lite/comp_off_requests/new.html.erb
264
+ - app/views/hr_lite/documents/index.html.erb
251
265
  - app/views/hr_lite/employee_profiles/show.html.erb
252
266
  - app/views/hr_lite/event_mailer/event.html.erb
253
267
  - app/views/hr_lite/event_mailer/event.text.erb
@@ -268,6 +282,7 @@ files:
268
282
  - app/views/hr_lite/leave_requests/index.html.erb
269
283
  - app/views/hr_lite/leave_requests/new.html.erb
270
284
  - app/views/hr_lite/leave_requests/show.html.erb
285
+ - app/views/hr_lite/loans/index.html.erb
271
286
  - app/views/hr_lite/org/_node.html.erb
272
287
  - app/views/hr_lite/org/show.html.erb
273
288
  - app/views/hr_lite/policies/index.html.erb
@@ -281,6 +296,7 @@ files:
281
296
  - app/views/hr_lite/salary_slips/show.html.erb
282
297
  - app/views/hr_lite/shared/_form_errors.html.erb
283
298
  - app/views/hr_lite/shared/_pagination.html.erb
299
+ - app/views/hr_lite/tax_declarations/show.html.erb
284
300
  - app/views/hr_lite/team/show.html.erb
285
301
  - app/views/layouts/hr_lite/application.html.erb
286
302
  - app/views/layouts/hr_lite/mailer.html.erb