hr_lite 0.1.1 → 0.2.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 +4 -4
- data/CHANGELOG.md +36 -0
- data/app/assets/stylesheets/hr_lite/hr_lite.css +42 -20
- data/app/controllers/hr_lite/admin/employees_controller.rb +49 -2
- data/app/controllers/hr_lite/admin/resignations_controller.rb +18 -0
- data/app/controllers/hr_lite/resignations_controller.rb +40 -0
- data/app/helpers/hr_lite/application_helper.rb +3 -23
- data/app/jobs/hr_lite/payroll_auto_draft_job.rb +23 -0
- data/app/models/hr_lite/employee_profile.rb +4 -0
- data/app/models/hr_lite/resignation.rb +84 -0
- data/app/models/hr_lite/setting.rb +11 -0
- data/app/views/hr_lite/admin/employees/_form.html.erb +20 -2
- data/app/views/hr_lite/admin/employees/index.html.erb +17 -0
- data/app/views/hr_lite/admin/employees/show.html.erb +42 -0
- data/app/views/hr_lite/employee_profiles/show.html.erb +1 -0
- data/app/views/hr_lite/resignations/show.html.erb +42 -0
- data/app/views/hr_lite/salary_slips/pdf.html.erb +6 -1
- data/app/views/layouts/hr_lite/application.html.erb +45 -16
- data/config/routes.rb +7 -0
- data/db/migrate/20260719065841_create_hr_lite_resignations.rb +16 -0
- data/lib/hr_lite/amount_in_words.rb +34 -0
- data/lib/hr_lite/configuration.rb +19 -1
- data/lib/hr_lite/notifications.rb +6 -1
- data/lib/hr_lite/version.rb +1 -1
- data/lib/hr_lite.rb +1 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a85f50e1151c664825892bd973f34842d1330e4878e39bb4984233880258c446
|
|
4
|
+
data.tar.gz: 95012fa0d0b7332e35e1fdef90ace239a95b415cbdc9d3101cb86fac18de9f7c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b532c9f0fe3b5408eea1e847c40329ab28da2a4c996d7cb229d1d745187fe33b5ace2fb17b8d1ea1ec1fc2e326cfc91dfd8881d296197a9d80bb40ded90a6f2
|
|
7
|
+
data.tar.gz: 64dfd72f81d71bd48ce01a4815c95360994dfb6b06235933ba893bb8245bdf4bd79ab384d7f7ca441fcec3ab158861c73d9bf8485b2478811e7df90d67443bb9
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-07-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Resignations: employees submit/withdraw from the portal; leadership
|
|
15
|
+
accepts with a confirmed last working day that stamps the profile's
|
|
16
|
+
exit date (payroll/attendance clip to it automatically).
|
|
17
|
+
- Onboarding: leadership creates the sign-in with the profile via the
|
|
18
|
+
`onboard_user` hook (no self sign-up anywhere); offboarding stamps the
|
|
19
|
+
exit date and revokes access via `offboard_user` — records are never
|
|
20
|
+
deleted.
|
|
21
|
+
- `PayrollAutoDraftJob`: monthly automation that drafts + computes the
|
|
22
|
+
previous month's payroll from attendance and notifies leadership for
|
|
23
|
+
review; publishing stays human.
|
|
24
|
+
- Company logo (`company[:logo_url]`, data-URIs welcome) in the shell
|
|
25
|
+
and on salary-slip PDFs.
|
|
26
|
+
- Keka-style left-rail navigation on desktop with grouped sections
|
|
27
|
+
(My work / Team / Organisation); mobile keeps the bottom tab bar.
|
|
28
|
+
- New notification-matrix events: `resignation.*`, `employee.onboarded`,
|
|
29
|
+
`payroll.draft_ready`.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- The auto-created settings row no longer emails leadership
|
|
34
|
+
("Someone created Setting" noise); real settings edits stay audited.
|
|
35
|
+
|
|
36
|
+
## [0.1.2] - 2026-07-19
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- Slip PDF template no longer 500s when rendered by host code
|
|
41
|
+
(`config.render_pdf`): amount-in-words is now a PORO
|
|
42
|
+
(`HrLite::AmountInWords`), not a view helper, since engine helpers are
|
|
43
|
+
not in scope under a host renderer. Caught live against a host app;
|
|
44
|
+
regression spec renders the template through a bare controller.
|
|
45
|
+
|
|
10
46
|
## [0.1.1] - 2026-07-19
|
|
11
47
|
|
|
12
48
|
### Added
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
overflow-x: hidden;
|
|
34
34
|
}
|
|
35
35
|
.hrl-body a { color: var(--hrl-accent); text-decoration: none; }
|
|
36
|
+
.hrl-shell { display: flex; min-height: 100vh; }
|
|
37
|
+
.hrl-content { flex: 1; min-width: 0; }
|
|
36
38
|
.hrl-main {
|
|
37
39
|
padding: .85rem;
|
|
38
40
|
padding-bottom: calc(4.4rem + env(safe-area-inset-bottom));
|
|
@@ -41,30 +43,26 @@
|
|
|
41
43
|
}
|
|
42
44
|
.hrl-main > * { min-width: 0; }
|
|
43
45
|
|
|
44
|
-
/*
|
|
45
|
-
.hrl-
|
|
46
|
-
background: var(--hrl-card);
|
|
47
|
-
border-bottom: 1px solid var(--hrl-line);
|
|
48
|
-
padding-top: env(safe-area-inset-top);
|
|
49
|
-
position: sticky; top: 0; z-index: 20;
|
|
50
|
-
}
|
|
51
|
-
.hrl-appbar__inner {
|
|
52
|
-
display: flex; align-items: center; gap: .75rem;
|
|
53
|
-
padding: .6rem .85rem; max-width: 68rem; margin: 0 auto;
|
|
54
|
-
}
|
|
55
|
-
.hrl-appbar__brand { font-weight: 700; font-size: 1.02rem; color: var(--hrl-ink); }
|
|
56
|
-
.hrl-appbar__user { margin-left: auto; color: var(--hrl-muted); font-size: .82rem; white-space: nowrap; }
|
|
57
|
-
|
|
58
|
-
/* Nav: hidden inline on phones (tabbar covers it), row on >=768px */
|
|
59
|
-
.hrl-nav--top { display: none; }
|
|
60
|
-
.hrl-nav__sep { width: 1px; align-self: stretch; background: var(--hrl-line); margin: 0 .2rem; }
|
|
46
|
+
/* Left rail (Keka-style) — hidden on phones, fixed column on >=768px */
|
|
47
|
+
.hrl-side { display: none; }
|
|
61
48
|
.hrl-nav__link {
|
|
62
49
|
color: var(--hrl-muted); font-size: .88rem; padding: .5rem .6rem;
|
|
63
50
|
border-radius: var(--hrl-radius-sm); min-height: 44px;
|
|
64
51
|
display: inline-flex; align-items: center;
|
|
65
52
|
}
|
|
66
53
|
.hrl-nav__link--active { color: var(--hrl-accent); background: var(--hrl-info-bg); font-weight: 600; }
|
|
67
|
-
|
|
54
|
+
|
|
55
|
+
/* Mobile app bar */
|
|
56
|
+
.hrl-appbar--mobile {
|
|
57
|
+
background: var(--hrl-card);
|
|
58
|
+
border-bottom: 1px solid var(--hrl-line);
|
|
59
|
+
padding: calc(.6rem + env(safe-area-inset-top)) .85rem .6rem;
|
|
60
|
+
position: sticky; top: 0; z-index: 20;
|
|
61
|
+
display: flex; align-items: center; gap: .75rem;
|
|
62
|
+
}
|
|
63
|
+
.hrl-appbar__brand { font-weight: 700; font-size: 1.02rem; color: var(--hrl-ink); display: inline-flex; align-items: center; }
|
|
64
|
+
.hrl-appbar__logo { height: 28px; width: auto; display: block; }
|
|
65
|
+
.hrl-appbar__user { margin-left: auto; color: var(--hrl-muted); font-size: .82rem; white-space: nowrap; }
|
|
68
66
|
|
|
69
67
|
/* Bottom tab bar (phone) */
|
|
70
68
|
.hrl-tabbar {
|
|
@@ -76,9 +74,33 @@
|
|
|
76
74
|
.hrl-tabbar .hrl-nav__link { flex: 1; justify-content: center; font-size: .78rem; }
|
|
77
75
|
|
|
78
76
|
@media (min-width: 768px) {
|
|
79
|
-
.hrl-nav--top { display: flex; flex-wrap: wrap; gap: .1rem; align-items: center; }
|
|
80
77
|
.hrl-tabbar { display: none; }
|
|
81
|
-
.hrl-
|
|
78
|
+
.hrl-appbar--mobile { display: none; }
|
|
79
|
+
.hrl-main { padding: 1.4rem 1.6rem 2rem; }
|
|
80
|
+
|
|
81
|
+
.hrl-side {
|
|
82
|
+
display: flex; flex-direction: column;
|
|
83
|
+
width: 232px; flex: 0 0 232px;
|
|
84
|
+
background: var(--hrl-card); border-right: 1px solid var(--hrl-line);
|
|
85
|
+
position: sticky; top: 0; height: 100vh; overflow-y: auto;
|
|
86
|
+
padding: 1rem .75rem;
|
|
87
|
+
}
|
|
88
|
+
.hrl-side__brand { display: block; padding: .25rem .5rem 1rem; }
|
|
89
|
+
.hrl-side__logo { max-height: 40px; max-width: 100%; display: block; }
|
|
90
|
+
.hrl-side__brandname { font-weight: 800; font-size: 1.1rem; color: var(--hrl-ink); }
|
|
91
|
+
.hrl-side__nav { display: flex; flex-direction: column; gap: 2px; }
|
|
92
|
+
.hrl-side__nav .hrl-nav__link { display: flex; min-height: 38px; padding: .45rem .6rem; }
|
|
93
|
+
.hrl-side__group {
|
|
94
|
+
margin: .9rem .5rem .25rem; font-size: .68rem; font-weight: 700;
|
|
95
|
+
letter-spacing: .08em; text-transform: uppercase; color: var(--hrl-muted);
|
|
96
|
+
}
|
|
97
|
+
.hrl-side__group:first-child { margin-top: 0; }
|
|
98
|
+
.hrl-side__foot {
|
|
99
|
+
margin-top: auto; padding: .75rem .5rem 0; border-top: 1px solid var(--hrl-line);
|
|
100
|
+
display: flex; flex-direction: column; gap: .35rem;
|
|
101
|
+
}
|
|
102
|
+
.hrl-side__user { font-size: .82rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
103
|
+
.hrl-side__back { font-size: .8rem; }
|
|
82
104
|
}
|
|
83
105
|
|
|
84
106
|
/* Flash */
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
module HrLite
|
|
2
2
|
module Admin
|
|
3
|
-
# Employee HR profiles (leadership).
|
|
3
|
+
# Employee HR profiles (leadership). Onboarding can create the login
|
|
4
|
+
# itself (config.onboard_user); offboarding stamps the exit date and
|
|
5
|
+
# revokes access (config.offboard_user) — nothing is ever deleted,
|
|
4
6
|
# payroll history is a statutory record.
|
|
5
7
|
class EmployeesController < LeadershipController
|
|
6
8
|
def index
|
|
7
9
|
@profiles = paginate(EmployeeProfile.includes(:user).order(:employee_code))
|
|
8
10
|
@users_without_profile = HrLite.employees.reject { |u| EmployeeProfile.exists?(user_id: u.id) }
|
|
11
|
+
@pending_resignations = Resignation.pending.includes(:user).recent_first
|
|
9
12
|
end
|
|
10
13
|
|
|
11
14
|
def show
|
|
12
15
|
@profile = EmployeeProfile.includes(:user).find(params[:id])
|
|
13
16
|
@structures = SalaryStructure.where(user_id: @profile.user_id).order(effective_from: :desc)
|
|
17
|
+
@pending_resignation = Resignation.pending.find_by(user_id: @profile.user_id)
|
|
14
18
|
end
|
|
15
19
|
|
|
16
20
|
def new
|
|
@@ -18,12 +22,39 @@ module HrLite
|
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
def create
|
|
25
|
+
new_login = params.dig(:employee_profile, :new_user_email).present?
|
|
21
26
|
@profile = EmployeeProfile.new(profile_params)
|
|
27
|
+
|
|
28
|
+
if new_login
|
|
29
|
+
user = HrLite.config.onboard_user.call(
|
|
30
|
+
name: params.dig(:employee_profile, :new_user_name).to_s,
|
|
31
|
+
email: params.dig(:employee_profile, :new_user_email).to_s.strip,
|
|
32
|
+
password: params.dig(:employee_profile, :new_user_password).to_s
|
|
33
|
+
)
|
|
34
|
+
@profile.user_id = user.id
|
|
35
|
+
end
|
|
36
|
+
|
|
22
37
|
if @profile.save
|
|
23
|
-
|
|
38
|
+
if new_login
|
|
39
|
+
Notifications.publish(
|
|
40
|
+
"employee.onboarded",
|
|
41
|
+
title: "Welcome aboard — your HR account is ready",
|
|
42
|
+
body: "Sign in with your email; your manager has your starting password.",
|
|
43
|
+
path: "/",
|
|
44
|
+
bell_to: [ @profile.user ],
|
|
45
|
+
email_to: [ @profile.user ]
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
redirect_to admin_employee_path(@profile), notice: "Employee #{'onboarded' if new_login} profile created."
|
|
24
49
|
else
|
|
25
50
|
render :new, status: :unprocessable_entity
|
|
26
51
|
end
|
|
52
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
53
|
+
@profile.errors.add(:base, "Could not create the login: #{e.record.errors.full_messages.to_sentence}")
|
|
54
|
+
render :new, status: :unprocessable_entity
|
|
55
|
+
rescue ActiveRecord::RecordNotUnique
|
|
56
|
+
@profile.errors.add(:base, "Could not create the login: that email already has an account")
|
|
57
|
+
render :new, status: :unprocessable_entity
|
|
27
58
|
end
|
|
28
59
|
|
|
29
60
|
def edit
|
|
@@ -39,6 +70,22 @@ module HrLite
|
|
|
39
70
|
end
|
|
40
71
|
end
|
|
41
72
|
|
|
73
|
+
# Exit: stamps the date (attendance/payroll clip on it) and revokes
|
|
74
|
+
# access via the host hook. Audited via the profile update.
|
|
75
|
+
def offboard
|
|
76
|
+
@profile = EmployeeProfile.find(params[:id])
|
|
77
|
+
exit_date = params[:date_of_exit].presence&.to_date || Date.current
|
|
78
|
+
|
|
79
|
+
@profile.update!(date_of_exit: exit_date)
|
|
80
|
+
begin
|
|
81
|
+
HrLite.config.offboard_user.call(@profile.user)
|
|
82
|
+
rescue => e
|
|
83
|
+
Rails.logger.error("[hr_lite] offboard_user failed: #{e.class}: #{e.message}")
|
|
84
|
+
end
|
|
85
|
+
redirect_to admin_employee_path(@profile),
|
|
86
|
+
notice: "Offboarded — last day #{exit_date.strftime('%d %b %Y')}, access revoked."
|
|
87
|
+
end
|
|
88
|
+
|
|
42
89
|
private
|
|
43
90
|
|
|
44
91
|
def profile_params
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
class ResignationsController < LeadershipController
|
|
4
|
+
def accept
|
|
5
|
+
resignation = Resignation.find(params[:id])
|
|
6
|
+
resignation.accept!(
|
|
7
|
+
actor: hr_current_user,
|
|
8
|
+
last_day: params[:last_day].presence&.to_date,
|
|
9
|
+
note: params[:note]
|
|
10
|
+
)
|
|
11
|
+
redirect_to admin_employees_path,
|
|
12
|
+
notice: "Resignation accepted — exit date recorded on the profile."
|
|
13
|
+
rescue ActiveRecord::RecordInvalid
|
|
14
|
+
redirect_to admin_employees_path, alert: "Only pending resignations can be accepted."
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Employee self-service resignation: submit, see status, withdraw while
|
|
3
|
+
# pending. Always scoped to hr_current_user.
|
|
4
|
+
class ResignationsController < ApplicationController
|
|
5
|
+
def show
|
|
6
|
+
@resignation = own.recent_first.first
|
|
7
|
+
@new_resignation = Resignation.new(proposed_last_day: Date.current + 30) unless @resignation&.pending?
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def create
|
|
11
|
+
@resignation = Resignation.new(resignation_params.merge(user_id: hr_current_user.id))
|
|
12
|
+
if @resignation.save
|
|
13
|
+
redirect_to resignation_path, notice: "Resignation submitted — leadership has been notified."
|
|
14
|
+
else
|
|
15
|
+
@new_resignation = @resignation
|
|
16
|
+
render :show, status: :unprocessable_entity
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def withdraw
|
|
21
|
+
resignation = own.pending.first
|
|
22
|
+
if resignation
|
|
23
|
+
resignation.withdraw!(actor: hr_current_user)
|
|
24
|
+
redirect_to resignation_path, notice: "Resignation withdrawn."
|
|
25
|
+
else
|
|
26
|
+
redirect_to resignation_path, alert: "Nothing pending to withdraw."
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def own
|
|
33
|
+
Resignation.where(user_id: hr_current_user.id)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def resignation_params
|
|
37
|
+
params.require(:resignation).permit(:reason, :proposed_last_day)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -80,30 +80,10 @@ module HrLite
|
|
|
80
80
|
"#{sign}#{HrLite.config.currency_symbol}#{whole}.#{fraction.ljust(2, '0')}"
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
TENS = %w[zero ten twenty thirty forty fifty sixty seventy eighty ninety].freeze
|
|
86
|
-
|
|
87
|
-
# Indian-system amount in words for the slip footer.
|
|
83
|
+
# Indian-system amount in words for the slip footer. Delegates to the
|
|
84
|
+
# PORO so host-rendered templates (config.render_pdf) work too.
|
|
88
85
|
def hrl_amount_in_words(amount)
|
|
89
|
-
|
|
90
|
-
return "Zero rupees" if rupees.zero?
|
|
91
|
-
|
|
92
|
-
parts = []
|
|
93
|
-
[ [ 10_000_000, "crore" ], [ 100_000, "lakh" ], [ 1000, "thousand" ], [ 100, "hundred" ] ].each do |divisor, label|
|
|
94
|
-
next unless rupees >= divisor
|
|
95
|
-
|
|
96
|
-
parts << "#{hrl_two_digit_words(rupees / divisor)} #{label}"
|
|
97
|
-
rupees %= divisor
|
|
98
|
-
end
|
|
99
|
-
parts << hrl_two_digit_words(rupees) if rupees.positive?
|
|
100
|
-
"#{parts.join(' ').capitalize} rupees"
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def hrl_two_digit_words(number)
|
|
104
|
-
return ONES[number] if number < 20
|
|
105
|
-
|
|
106
|
-
[ TENS[number / 10], number % 10 == 0 ? nil : ONES[number % 10] ].compact.join("-")
|
|
86
|
+
HrLite::AmountInWords.words(amount)
|
|
107
87
|
end
|
|
108
88
|
end
|
|
109
89
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Monthly automation: on the 1st, draft + compute the previous month's
|
|
3
|
+
# payroll from attendance and the policy, then tell leadership it is
|
|
4
|
+
# waiting for review. Publishing stays a deliberate human action —
|
|
5
|
+
# the system prepares, people approve.
|
|
6
|
+
class PayrollAutoDraftJob < ActiveJob::Base
|
|
7
|
+
queue_as :default
|
|
8
|
+
|
|
9
|
+
def perform(month: Date.current.prev_month.beginning_of_month)
|
|
10
|
+
return unless EmployeeProfile.active_for(month).exists?
|
|
11
|
+
|
|
12
|
+
run = PayrollRun.find_or_create_by!(period_month: month)
|
|
13
|
+
return unless run.draft? || run.review?
|
|
14
|
+
|
|
15
|
+
run.compute!(actor: nil)
|
|
16
|
+
Notifications.publish(
|
|
17
|
+
"payroll.draft_ready",
|
|
18
|
+
title: "Payroll #{run.label} computed from attendance — #{run.salary_slips.count} slips await review",
|
|
19
|
+
path: "/admin/payroll_runs/#{run.id}"
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -14,6 +14,10 @@ module HrLite
|
|
|
14
14
|
|
|
15
15
|
TAX_REGIMES = %w[new old].freeze
|
|
16
16
|
|
|
17
|
+
# Onboarding-form virtuals (the controller hands them to
|
|
18
|
+
# config.onboard_user; never persisted, never audited).
|
|
19
|
+
attr_accessor :new_user_name, :new_user_email, :new_user_password
|
|
20
|
+
|
|
17
21
|
validates :employee_code, presence: true, uniqueness: true
|
|
18
22
|
validates :user_id, uniqueness: true
|
|
19
23
|
validates :date_of_joining, presence: true
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Employee-initiated exit. One open resignation per person; accepting it
|
|
3
|
+
# stamps the employee profile's exit date (which payroll and attendance
|
|
4
|
+
# already respect) and notifies both sides. Withdrawal is allowed while
|
|
5
|
+
# pending — leaving should never need an email thread.
|
|
6
|
+
class Resignation < ApplicationRecord
|
|
7
|
+
STATUSES = %w[pending accepted withdrawn].freeze
|
|
8
|
+
|
|
9
|
+
belongs_to :user, class_name: HrLite.config.user_class
|
|
10
|
+
belongs_to :decided_by, class_name: HrLite.config.user_class, optional: true
|
|
11
|
+
|
|
12
|
+
validates :proposed_last_day, presence: true
|
|
13
|
+
validates :status, inclusion: { in: STATUSES }
|
|
14
|
+
validate :last_day_not_past, on: :create
|
|
15
|
+
validate :single_open_resignation, on: :create
|
|
16
|
+
|
|
17
|
+
scope :pending, -> { where(status: "pending") }
|
|
18
|
+
scope :recent_first, -> { order(created_at: :desc) }
|
|
19
|
+
|
|
20
|
+
STATUSES.each { |s| define_method("#{s}?") { status == s } }
|
|
21
|
+
|
|
22
|
+
after_create :notify_submitted
|
|
23
|
+
|
|
24
|
+
def accept!(actor:, last_day: nil, note: nil)
|
|
25
|
+
raise ActiveRecord::RecordInvalid.new(self), "not pending" unless pending?
|
|
26
|
+
|
|
27
|
+
final_day = last_day.presence || proposed_last_day
|
|
28
|
+
transaction do
|
|
29
|
+
update!(status: "accepted", decided_by_id: actor.id, decided_at: Time.current,
|
|
30
|
+
decision_note: note.presence, proposed_last_day: final_day)
|
|
31
|
+
profile = EmployeeProfile.find_by(user_id: user_id)
|
|
32
|
+
profile&.update!(date_of_exit: final_day)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Notifications.publish(
|
|
36
|
+
"resignation.accepted",
|
|
37
|
+
title: "Resignation accepted — last working day #{final_day.strftime('%d %b %Y')}",
|
|
38
|
+
body: note.presence,
|
|
39
|
+
path: "/resignation",
|
|
40
|
+
bell_to: [ user ],
|
|
41
|
+
email_to: [ user ]
|
|
42
|
+
)
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def withdraw!(actor:)
|
|
47
|
+
raise ActiveRecord::RecordInvalid.new(self), "not pending" unless pending? && actor.id == user_id
|
|
48
|
+
|
|
49
|
+
update!(status: "withdrawn", decided_at: Time.current)
|
|
50
|
+
Notifications.publish(
|
|
51
|
+
"resignation.withdrawn",
|
|
52
|
+
title: "#{HrLite.display_name(user)} withdrew their resignation",
|
|
53
|
+
path: "/admin/employees",
|
|
54
|
+
bell_to: HrLite.admin_users
|
|
55
|
+
)
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def notify_submitted
|
|
62
|
+
Notifications.publish(
|
|
63
|
+
"resignation.submitted",
|
|
64
|
+
title: "#{HrLite.display_name(user)} submitted their resignation " \
|
|
65
|
+
"(last day proposed: #{proposed_last_day.strftime('%d %b %Y')})",
|
|
66
|
+
body: reason.presence,
|
|
67
|
+
path: "/admin/employees",
|
|
68
|
+
bell_to: HrLite.admin_users
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def last_day_not_past
|
|
73
|
+
return unless proposed_last_day
|
|
74
|
+
|
|
75
|
+
errors.add(:proposed_last_day, "cannot be in the past") if proposed_last_day < Date.current
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def single_open_resignation
|
|
79
|
+
if self.class.pending.where(user_id: user_id).exists?
|
|
80
|
+
errors.add(:base, "You already have a pending resignation")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -10,5 +10,16 @@ module HrLite
|
|
|
10
10
|
def self.instance
|
|
11
11
|
first_or_create!
|
|
12
12
|
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# The defaults row bootstraps itself on first read — a system action,
|
|
17
|
+
# not a governing change; auditing it would email leadership noise.
|
|
18
|
+
# Real edits (updates) stay fully audited.
|
|
19
|
+
def hr_lite_audit!(action)
|
|
20
|
+
return if action == "create" && HrLite::Current.actor.nil?
|
|
21
|
+
|
|
22
|
+
super
|
|
23
|
+
end
|
|
13
24
|
end
|
|
14
25
|
end
|
|
@@ -4,9 +4,27 @@
|
|
|
4
4
|
<%= f.hidden_field :user_id %>
|
|
5
5
|
<% if profile.new_record? && profile.user_id.blank? %>
|
|
6
6
|
<div class="hrl-field">
|
|
7
|
-
<%= f.label :user_id, "
|
|
8
|
-
<%= f.select :user_id, HrLite.employees.map { |u| [ HrLite.display_name(u), u.id ] }
|
|
7
|
+
<%= f.label :user_id, "Existing staff member" %>
|
|
8
|
+
<%= f.select :user_id, HrLite.employees.map { |u| [ HrLite.display_name(u), u.id ] },
|
|
9
|
+
include_blank: "— onboard a brand-new person below —" %>
|
|
9
10
|
</div>
|
|
11
|
+
<fieldset class="hrl-card" style="margin-bottom:.8rem;">
|
|
12
|
+
<legend class="hrl-card__title">New login (onboarding)</legend>
|
|
13
|
+
<p class="hrl-hint">Fill these ONLY for someone who has no account yet — their sign-in is
|
|
14
|
+
created with the profile; there is no self sign-up.</p>
|
|
15
|
+
<div class="hrl-field">
|
|
16
|
+
<%= f.label :new_user_name, "Full name" %>
|
|
17
|
+
<%= f.text_field :new_user_name %>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="hrl-field">
|
|
20
|
+
<%= f.label :new_user_email, "Work email (their sign-in)" %>
|
|
21
|
+
<%= f.email_field :new_user_email %>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="hrl-field">
|
|
24
|
+
<%= f.label :new_user_password, "Starting password (share it with them; they can change it later)" %>
|
|
25
|
+
<%= f.text_field :new_user_password %>
|
|
26
|
+
</div>
|
|
27
|
+
</fieldset>
|
|
10
28
|
<% end %>
|
|
11
29
|
<div class="hrl-field"><%= f.label :employee_code %><%= f.text_field :employee_code %></div>
|
|
12
30
|
<div class="hrl-field"><%= f.label :designation %><%= f.text_field :designation %></div>
|
|
@@ -28,6 +28,23 @@
|
|
|
28
28
|
<% end %>
|
|
29
29
|
</section>
|
|
30
30
|
|
|
31
|
+
<% if @pending_resignations.any? %>
|
|
32
|
+
<section class="hrl-card" style="border-color: var(--hrl-warn);">
|
|
33
|
+
<h2 class="hrl-card__title">Pending resignations</h2>
|
|
34
|
+
<% @pending_resignations.each do |resignation| %>
|
|
35
|
+
<div class="hrl-feed__item">
|
|
36
|
+
<div class="hrl-feed__body">
|
|
37
|
+
<strong><%= hr_display_name(resignation.user) %></strong>
|
|
38
|
+
— proposed last day <%= resignation.proposed_last_day.strftime("%d %b %Y") %>
|
|
39
|
+
<% if resignation.reason.present? %><span class="hrl-small hrl-muted"><%= resignation.reason.truncate(80) %></span><% end %>
|
|
40
|
+
</div>
|
|
41
|
+
<% profile = HrLite::EmployeeProfile.find_by(user_id: resignation.user_id) %>
|
|
42
|
+
<% if profile %><a class="hrl-btn" href="<%= hr_lite.admin_employee_path(profile) %>">Decide</a><% end %>
|
|
43
|
+
</div>
|
|
44
|
+
<% end %>
|
|
45
|
+
</section>
|
|
46
|
+
<% end %>
|
|
47
|
+
|
|
31
48
|
<% if @users_without_profile.any? %>
|
|
32
49
|
<section class="hrl-card">
|
|
33
50
|
<h2 class="hrl-card__title">Staff without an HR profile</h2>
|
|
@@ -9,6 +9,48 @@
|
|
|
9
9
|
</div>
|
|
10
10
|
</div>
|
|
11
11
|
|
|
12
|
+
<% if @pending_resignation %>
|
|
13
|
+
<section class="hrl-card" style="border-color: var(--hrl-warn);">
|
|
14
|
+
<h2 class="hrl-card__title">Pending resignation</h2>
|
|
15
|
+
<dl class="hrl-deflist">
|
|
16
|
+
<dt>Proposed last day</dt><dd><%= @pending_resignation.proposed_last_day.strftime("%d %B %Y") %></dd>
|
|
17
|
+
<% if @pending_resignation.reason.present? %><dt>Reason</dt><dd><%= @pending_resignation.reason %></dd><% end %>
|
|
18
|
+
</dl>
|
|
19
|
+
<%= form_with url: hr_lite.accept_admin_resignation_path(@pending_resignation), method: :post, local: true do %>
|
|
20
|
+
<div class="hrl-field">
|
|
21
|
+
<label for="res_last_day">Confirm last working day</label>
|
|
22
|
+
<input type="date" id="res_last_day" name="last_day" class="hrl-input"
|
|
23
|
+
value="<%= @pending_resignation.proposed_last_day %>">
|
|
24
|
+
</div>
|
|
25
|
+
<div class="hrl-field">
|
|
26
|
+
<label for="res_note">Note to the employee (optional)</label>
|
|
27
|
+
<input type="text" id="res_note" name="note" class="hrl-input">
|
|
28
|
+
</div>
|
|
29
|
+
<div class="hrl-form-actions">
|
|
30
|
+
<button class="hrl-btn hrl-btn--primary" type="submit">Accept resignation</button>
|
|
31
|
+
</div>
|
|
32
|
+
<% end %>
|
|
33
|
+
</section>
|
|
34
|
+
<% end %>
|
|
35
|
+
|
|
36
|
+
<% if @profile.date_of_exit.nil? %>
|
|
37
|
+
<section class="hrl-card">
|
|
38
|
+
<h2 class="hrl-card__title">Offboard</h2>
|
|
39
|
+
<p class="hrl-small hrl-muted">Stamps the last day (attendance and payroll clip to it) and revokes sign-in.
|
|
40
|
+
Records are never deleted.</p>
|
|
41
|
+
<%= form_with url: hr_lite.offboard_admin_employee_path(@profile), method: :post, local: true,
|
|
42
|
+
html: { data: { turbo_confirm: "Offboard #{hr_display_name(@profile.user)}?" } } do %>
|
|
43
|
+
<div class="hrl-field">
|
|
44
|
+
<label for="off_date">Last working day</label>
|
|
45
|
+
<input type="date" id="off_date" name="date_of_exit" class="hrl-input" value="<%= Date.current %>">
|
|
46
|
+
</div>
|
|
47
|
+
<div class="hrl-form-actions">
|
|
48
|
+
<button class="hrl-btn hrl-btn--danger" type="submit">Offboard employee</button>
|
|
49
|
+
</div>
|
|
50
|
+
<% end %>
|
|
51
|
+
</section>
|
|
52
|
+
<% end %>
|
|
53
|
+
|
|
12
54
|
<div class="hrl-grid hrl-grid--2">
|
|
13
55
|
<section class="hrl-card">
|
|
14
56
|
<h2 class="hrl-card__title">Profile</h2>
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
<dt>Bank</dt><dd><%= [ @profile.bank_name, @profile.masked_account ].compact.join(" ").presence || "—" %></dd>
|
|
17
17
|
</dl>
|
|
18
18
|
<p class="hrl-small hrl-muted hrl-mt">Something wrong? Ask leadership to correct it — changes are audited.</p>
|
|
19
|
+
<a class="hrl-btn hrl-mt" href="<%= hr_lite.resignation_path %>">Resignation</a>
|
|
19
20
|
</section>
|
|
20
21
|
<% else %>
|
|
21
22
|
<p class="hrl-card hrl-card--empty">Your HR profile has not been set up yet.</p>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Resignation" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Resignation</h1>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<% if @resignation %>
|
|
7
|
+
<section class="hrl-card">
|
|
8
|
+
<dl class="hrl-deflist">
|
|
9
|
+
<dt>Status</dt>
|
|
10
|
+
<dd><span class="hrl-badge <%= { 'accepted' => 'hrl-badge--warn', 'withdrawn' => 'hrl-badge--muted' }[@resignation.status] %>"><%= @resignation.status.humanize %></span></dd>
|
|
11
|
+
<dt>Last working day</dt><dd><%= @resignation.proposed_last_day.strftime("%d %B %Y") %></dd>
|
|
12
|
+
<% if @resignation.reason.present? %><dt>Reason</dt><dd><%= @resignation.reason %></dd><% end %>
|
|
13
|
+
<% if @resignation.decision_note.present? %><dt>Leadership note</dt><dd><%= @resignation.decision_note %></dd><% end %>
|
|
14
|
+
</dl>
|
|
15
|
+
<% if @resignation.pending? %>
|
|
16
|
+
<%= button_to "Withdraw resignation", hr_lite.withdraw_resignation_path, method: :post,
|
|
17
|
+
class: "hrl-btn hrl-btn--danger hrl-mt",
|
|
18
|
+
form: { data: { turbo_confirm: "Withdraw your resignation?" } } %>
|
|
19
|
+
<% end %>
|
|
20
|
+
</section>
|
|
21
|
+
<% end %>
|
|
22
|
+
|
|
23
|
+
<% if @new_resignation %>
|
|
24
|
+
<section class="hrl-card">
|
|
25
|
+
<h2 class="hrl-card__title">Submit resignation</h2>
|
|
26
|
+
<p class="hrl-small hrl-muted">Leadership is notified immediately and will confirm your last working day.</p>
|
|
27
|
+
<%= form_with model: @new_resignation, url: hr_lite.resignation_path, local: true do |f| %>
|
|
28
|
+
<%= render "hr_lite/shared/form_errors", record: @new_resignation %>
|
|
29
|
+
<div class="hrl-field">
|
|
30
|
+
<%= f.label :proposed_last_day, "Proposed last working day" %>
|
|
31
|
+
<%= f.date_field :proposed_last_day %>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="hrl-field">
|
|
34
|
+
<%= f.label :reason, "Reason (optional)" %>
|
|
35
|
+
<%= f.text_area :reason %>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="hrl-form-actions">
|
|
38
|
+
<%= f.submit "Submit resignation", class: "hrl-btn hrl-btn--primary" %>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
41
|
+
</section>
|
|
42
|
+
<% end %>
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
<div class="pdf-card">
|
|
3
3
|
<div class="pdf-head">
|
|
4
4
|
<div>
|
|
5
|
+
<% if company[:logo_url].present? %>
|
|
6
|
+
<img src="<%= company[:logo_url] %>" alt="<%= company[:name] %>" style="height:34px; display:block; margin-bottom:6px;">
|
|
7
|
+
<% end %>
|
|
5
8
|
<div class="pdf-company"><%= company[:name] %></div>
|
|
6
9
|
<% if company[:address].present? %><div class="pdf-muted"><%= company[:address] %></div><% end %>
|
|
7
10
|
</div>
|
|
@@ -61,8 +64,10 @@
|
|
|
61
64
|
</div>
|
|
62
65
|
|
|
63
66
|
<div class="pdf-net">
|
|
67
|
+
<%# PORO, not a helper — this template is rendered by HOST code via
|
|
68
|
+
config.render_pdf, where engine helper modules are not in scope. %>
|
|
64
69
|
Net pay: <%= HrLite.config.currency_symbol %><%= @slip.net_pay.to_s("F") %>
|
|
65
|
-
(<%=
|
|
70
|
+
(<%= HrLite::AmountInWords.words(@slip.net_pay) %>)
|
|
66
71
|
</div>
|
|
67
72
|
|
|
68
73
|
<p class="pdf-foot">
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
|
-
|
|
4
|
+
<% company = HrLite.config.company.call %>
|
|
5
|
+
<title><%= [ content_for(:page_title), "#{company[:name].presence || 'HR'} — HR" ].compact.join(" · ") %></title>
|
|
5
6
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
7
|
<%= csrf_meta_tags %>
|
|
7
8
|
<%= csp_meta_tag %>
|
|
@@ -12,37 +13,65 @@
|
|
|
12
13
|
<% end %>
|
|
13
14
|
</head>
|
|
14
15
|
<body class="hrl-body">
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<
|
|
16
|
+
<div class="hrl-shell">
|
|
17
|
+
<%# Keka-style left rail on desktop; slides away on phones (tabbar takes over). %>
|
|
18
|
+
<aside class="hrl-side" aria-label="HR navigation">
|
|
19
|
+
<a class="hrl-side__brand" href="<%= hr_lite.root_path %>">
|
|
20
|
+
<% if company[:logo_url].present? %>
|
|
21
|
+
<img class="hrl-side__logo" src="<%= company[:logo_url] %>" alt="<%= company[:name] %>">
|
|
22
|
+
<% else %>
|
|
23
|
+
<span class="hrl-side__brandname"><%= company[:name].presence || "HR" %></span>
|
|
24
|
+
<% end %>
|
|
25
|
+
</a>
|
|
26
|
+
|
|
27
|
+
<nav class="hrl-side__nav">
|
|
28
|
+
<span class="hrl-side__group">My work</span>
|
|
19
29
|
<% hrl_nav_items(HrLite::ApplicationHelper::NAV_ITEMS).each do |item| %>
|
|
20
30
|
<%= hrl_nav_link(item) %>
|
|
21
31
|
<% end %>
|
|
32
|
+
|
|
22
33
|
<% if hr_admin? || hr_leadership? %>
|
|
23
|
-
<span class="hrl-
|
|
34
|
+
<span class="hrl-side__group">Team</span>
|
|
24
35
|
<% hrl_nav_items(HrLite::ApplicationHelper::ADMIN_NAV_ITEMS).each do |item| %>
|
|
25
36
|
<%= hrl_nav_link(item) %>
|
|
26
37
|
<% end %>
|
|
27
38
|
<% end %>
|
|
39
|
+
|
|
28
40
|
<% if hr_leadership? %>
|
|
41
|
+
<span class="hrl-side__group">Organisation</span>
|
|
29
42
|
<% hrl_nav_items(HrLite::ApplicationHelper::LEADERSHIP_NAV_ITEMS).each do |item| %>
|
|
30
43
|
<%= hrl_nav_link(item) %>
|
|
31
44
|
<% end %>
|
|
32
45
|
<% end %>
|
|
46
|
+
</nav>
|
|
47
|
+
|
|
48
|
+
<div class="hrl-side__foot">
|
|
49
|
+
<span class="hrl-side__user" title="<%= hr_current_user&.email %>"><%= hr_display_name(hr_current_user) %></span>
|
|
33
50
|
<% if HrLite.config.back_link %>
|
|
34
|
-
<a class="hrl-
|
|
51
|
+
<a class="hrl-side__back" href="<%= HrLite.config.back_link[:url] %>"><%= HrLite.config.back_link[:label] %></a>
|
|
35
52
|
<% end %>
|
|
36
|
-
</
|
|
37
|
-
|
|
38
|
-
</div>
|
|
39
|
-
</header>
|
|
53
|
+
</div>
|
|
54
|
+
</aside>
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
<div class="hrl-content">
|
|
57
|
+
<header class="hrl-appbar hrl-appbar--mobile">
|
|
58
|
+
<a class="hrl-appbar__brand" href="<%= hr_lite.root_path %>">
|
|
59
|
+
<% if company[:logo_url].present? %>
|
|
60
|
+
<img class="hrl-appbar__logo" src="<%= company[:logo_url] %>" alt="<%= company[:name] %>">
|
|
61
|
+
<% else %>
|
|
62
|
+
<%= company[:name].presence || "HR" %>
|
|
63
|
+
<% end %>
|
|
64
|
+
</a>
|
|
65
|
+
<span class="hrl-appbar__user"><%= hr_display_name(hr_current_user) %></span>
|
|
66
|
+
</header>
|
|
67
|
+
|
|
68
|
+
<main class="hrl-main">
|
|
69
|
+
<% if flash[:notice].present? %><div class="hrl-flash hrl-flash--ok" role="status"><%= flash[:notice] %></div><% end %>
|
|
70
|
+
<% if flash[:alert].present? %><div class="hrl-flash hrl-flash--warn" role="alert"><%= flash[:alert] %></div><% end %>
|
|
71
|
+
<%= yield %>
|
|
72
|
+
</main>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
46
75
|
|
|
47
76
|
<nav class="hrl-tabbar" aria-label="HR sections">
|
|
48
77
|
<% hrl_nav_items(HrLite::ApplicationHelper::NAV_ITEMS).first(5).each do |item| %>
|
data/config/routes.rb
CHANGED
|
@@ -17,6 +17,9 @@ HrLite::Engine.routes.draw do
|
|
|
17
17
|
get "calendar", to: "calendar#show"
|
|
18
18
|
resources :salary_slips, only: %i[index show]
|
|
19
19
|
resource :employee_profile, only: :show, path: "profile"
|
|
20
|
+
resource :resignation, only: %i[show create], controller: "resignations" do
|
|
21
|
+
post :withdraw
|
|
22
|
+
end
|
|
20
23
|
get "career", to: "career#show"
|
|
21
24
|
resources :appraisals, only: %i[index show]
|
|
22
25
|
|
|
@@ -34,7 +37,11 @@ HrLite::Engine.routes.draw do
|
|
|
34
37
|
resources :holidays, only: %i[index create update destroy] do
|
|
35
38
|
collection { post :bulk_create }
|
|
36
39
|
end
|
|
40
|
+
resources :resignations, only: [] do
|
|
41
|
+
member { post :accept }
|
|
42
|
+
end
|
|
37
43
|
resources :employees, except: :destroy do
|
|
44
|
+
member { post :offboard }
|
|
38
45
|
resources :salary_structures, only: %i[new create edit update]
|
|
39
46
|
resources :appraisals, only: %i[new create edit update destroy] do
|
|
40
47
|
member { post :share }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class CreateHrLiteResignations < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :hr_lite_resignations do |t|
|
|
4
|
+
t.bigint :user_id, null: false
|
|
5
|
+
t.text :reason
|
|
6
|
+
t.date :proposed_last_day, null: false
|
|
7
|
+
t.string :status, null: false, default: "pending"
|
|
8
|
+
t.bigint :decided_by_id
|
|
9
|
+
t.datetime :decided_at
|
|
10
|
+
t.text :decision_note
|
|
11
|
+
|
|
12
|
+
t.timestamps
|
|
13
|
+
end
|
|
14
|
+
add_index :hr_lite_resignations, [ :user_id, :status ]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Indian-system amount in words (crore/lakh/thousand). A PORO — not a view
|
|
3
|
+
# helper — because the slip PDF template is rendered by HOST code
|
|
4
|
+
# (config.render_pdf), where engine helper modules are not in scope.
|
|
5
|
+
module AmountInWords
|
|
6
|
+
ONES = %w[zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen
|
|
7
|
+
fifteen sixteen seventeen eighteen nineteen].freeze
|
|
8
|
+
TENS = %w[zero ten twenty thirty forty fifty sixty seventy eighty ninety].freeze
|
|
9
|
+
SCALES = [ [ 10_000_000, "crore" ], [ 100_000, "lakh" ], [ 1000, "thousand" ], [ 100, "hundred" ] ].freeze
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def words(amount)
|
|
14
|
+
rupees = Money.round_rupee(amount).to_i
|
|
15
|
+
return "Zero rupees" if rupees.zero?
|
|
16
|
+
|
|
17
|
+
parts = []
|
|
18
|
+
SCALES.each do |divisor, label|
|
|
19
|
+
next unless rupees >= divisor
|
|
20
|
+
|
|
21
|
+
parts << "#{two_digit(rupees / divisor)} #{label}"
|
|
22
|
+
rupees %= divisor
|
|
23
|
+
end
|
|
24
|
+
parts << two_digit(rupees) if rupees.positive?
|
|
25
|
+
"#{parts.join(' ').capitalize} rupees"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def two_digit(number)
|
|
29
|
+
return ONES[number] if number < 20
|
|
30
|
+
|
|
31
|
+
[ TENS[number / 10], number % 10 == 0 ? nil : ONES[number % 10] ].compact.join("-")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -8,7 +8,8 @@ module HrLite
|
|
|
8
8
|
:employees_scope, :mentionable_users, :notify, :render_pdf, :company,
|
|
9
9
|
:time_zone, :currency_symbol, :on_designation_change,
|
|
10
10
|
:leadership_emails, :leadership_check, :extra_stylesheets,
|
|
11
|
-
:mailer_from, :public_url_base, :notification_matrix, :back_link
|
|
11
|
+
:mailer_from, :public_url_base, :notification_matrix, :back_link,
|
|
12
|
+
:onboard_user, :offboard_user
|
|
12
13
|
|
|
13
14
|
# 0.1.0 pre-release name for public_url_base; kept as an alias so early
|
|
14
15
|
# adopters' initializers don't break.
|
|
@@ -40,6 +41,23 @@ module HrLite
|
|
|
40
41
|
@public_url_base = nil # e.g. "https://hr.example.com" — enables email links + HrLite.public_url
|
|
41
42
|
@notification_matrix = nil # resolved lazily to Notifications::DEFAULT_MATRIX
|
|
42
43
|
@back_link = nil # optional {label:, url:} for the shell nav
|
|
44
|
+
|
|
45
|
+
# Leadership onboarding/offboarding. onboard_user must return a saved
|
|
46
|
+
# user record (default: create on user_class with whatever of
|
|
47
|
+
# name/email/password it supports). offboard_user should revoke the
|
|
48
|
+
# person's access — the engine never deletes anything (statutory
|
|
49
|
+
# records), it only stamps the exit date.
|
|
50
|
+
@onboard_user = ->(name:, email:, password:) {
|
|
51
|
+
klass = HrLite.user_klass
|
|
52
|
+
attributes = { email: email }
|
|
53
|
+
attributes[:name] = name if klass.column_names.include?("name")
|
|
54
|
+
if klass.method_defined?(:password=)
|
|
55
|
+
attributes[:password] = password
|
|
56
|
+
attributes[:password_confirmation] = password if klass.method_defined?(:password_confirmation=)
|
|
57
|
+
end
|
|
58
|
+
klass.create!(attributes)
|
|
59
|
+
}
|
|
60
|
+
@offboard_user = ->(user) { }
|
|
43
61
|
end
|
|
44
62
|
end
|
|
45
63
|
|
|
@@ -24,7 +24,12 @@ module HrLite
|
|
|
24
24
|
"appraisal.shared" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
|
|
25
25
|
"promotion.recorded" => { bell: true, email: true, leadership_email: true, leadership_bell: true },
|
|
26
26
|
"policy.changed" => { bell: false, email: false, leadership_email: true, leadership_bell: true },
|
|
27
|
-
"digest.daily" => { bell: false, email: false, leadership_email: true, leadership_bell: false }
|
|
27
|
+
"digest.daily" => { bell: false, email: false, leadership_email: true, leadership_bell: false },
|
|
28
|
+
"resignation.submitted" => { bell: true, email: false, leadership_email: true, leadership_bell: true },
|
|
29
|
+
"resignation.accepted" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
|
|
30
|
+
"resignation.withdrawn" => { bell: true, email: false, leadership_email: true, leadership_bell: false },
|
|
31
|
+
"employee.onboarded" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
|
|
32
|
+
"payroll.draft_ready" => { bell: false, email: false, leadership_email: true, leadership_bell: true }
|
|
28
33
|
}.freeze
|
|
29
34
|
|
|
30
35
|
class << self
|
data/lib/hr_lite/version.rb
CHANGED
data/lib/hr_lite.rb
CHANGED
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.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kshitiz sinha
|
|
@@ -68,6 +68,7 @@ files:
|
|
|
68
68
|
- app/controllers/hr_lite/admin/office_locations_controller.rb
|
|
69
69
|
- app/controllers/hr_lite/admin/overview_controller.rb
|
|
70
70
|
- app/controllers/hr_lite/admin/payroll_runs_controller.rb
|
|
71
|
+
- app/controllers/hr_lite/admin/resignations_controller.rb
|
|
71
72
|
- app/controllers/hr_lite/admin/salary_slips_controller.rb
|
|
72
73
|
- app/controllers/hr_lite/admin/salary_structures_controller.rb
|
|
73
74
|
- app/controllers/hr_lite/admin/settings_controller.rb
|
|
@@ -82,12 +83,14 @@ files:
|
|
|
82
83
|
- app/controllers/hr_lite/kudos_controller.rb
|
|
83
84
|
- app/controllers/hr_lite/leave_balances_controller.rb
|
|
84
85
|
- app/controllers/hr_lite/leave_requests_controller.rb
|
|
86
|
+
- app/controllers/hr_lite/resignations_controller.rb
|
|
85
87
|
- app/controllers/hr_lite/salary_slips_controller.rb
|
|
86
88
|
- app/controllers/hr_lite/users_controller.rb
|
|
87
89
|
- app/helpers/hr_lite/application_helper.rb
|
|
88
90
|
- app/jobs/hr_lite/application_job.rb
|
|
89
91
|
- app/jobs/hr_lite/daily_digest_job.rb
|
|
90
92
|
- app/jobs/hr_lite/leave_year_rollover_job.rb
|
|
93
|
+
- app/jobs/hr_lite/payroll_auto_draft_job.rb
|
|
91
94
|
- app/mailers/hr_lite/application_mailer.rb
|
|
92
95
|
- app/mailers/hr_lite/event_mailer.rb
|
|
93
96
|
- app/models/concerns/hr_lite/audited.rb
|
|
@@ -106,6 +109,7 @@ files:
|
|
|
106
109
|
- app/models/hr_lite/leave_type.rb
|
|
107
110
|
- app/models/hr_lite/office_location.rb
|
|
108
111
|
- app/models/hr_lite/payroll_run.rb
|
|
112
|
+
- app/models/hr_lite/resignation.rb
|
|
109
113
|
- app/models/hr_lite/salary_slip.rb
|
|
110
114
|
- app/models/hr_lite/salary_structure.rb
|
|
111
115
|
- app/models/hr_lite/setting.rb
|
|
@@ -178,6 +182,7 @@ files:
|
|
|
178
182
|
- app/views/hr_lite/leave_requests/index.html.erb
|
|
179
183
|
- app/views/hr_lite/leave_requests/new.html.erb
|
|
180
184
|
- app/views/hr_lite/leave_requests/show.html.erb
|
|
185
|
+
- app/views/hr_lite/resignations/show.html.erb
|
|
181
186
|
- app/views/hr_lite/salary_slips/_slip_detail.html.erb
|
|
182
187
|
- app/views/hr_lite/salary_slips/index.html.erb
|
|
183
188
|
- app/views/hr_lite/salary_slips/pdf.html.erb
|
|
@@ -205,10 +210,12 @@ files:
|
|
|
205
210
|
- db/migrate/20260718194537_create_hr_lite_salary_slips.rb
|
|
206
211
|
- db/migrate/20260718195953_create_hr_lite_appraisals.rb
|
|
207
212
|
- db/migrate/20260718195954_create_hr_lite_designation_changes.rb
|
|
213
|
+
- db/migrate/20260719065841_create_hr_lite_resignations.rb
|
|
208
214
|
- lib/generators/hr_lite/install/install_generator.rb
|
|
209
215
|
- lib/generators/hr_lite/install/templates/AFTER_INSTALL
|
|
210
216
|
- lib/generators/hr_lite/install/templates/initializer.rb
|
|
211
217
|
- lib/hr_lite.rb
|
|
218
|
+
- lib/hr_lite/amount_in_words.rb
|
|
212
219
|
- lib/hr_lite/configuration.rb
|
|
213
220
|
- lib/hr_lite/current.rb
|
|
214
221
|
- lib/hr_lite/engine.rb
|