hr_lite 0.1.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 +7 -0
- data/CHANGELOG.md +33 -0
- data/MIT-LICENSE +20 -0
- data/README.md +166 -0
- data/Rakefile +3 -0
- data/app/assets/javascripts/hr_lite/geo_punch.js +61 -0
- data/app/assets/javascripts/hr_lite/mention.js +100 -0
- data/app/assets/stylesheets/hr_lite/application.css +15 -0
- data/app/assets/stylesheets/hr_lite/hr_lite.css +239 -0
- data/app/controllers/hr_lite/admin/appraisals_controller.rb +74 -0
- data/app/controllers/hr_lite/admin/attendances_controller.rb +79 -0
- data/app/controllers/hr_lite/admin/audit_logs_controller.rb +9 -0
- data/app/controllers/hr_lite/admin/base_controller.rb +16 -0
- data/app/controllers/hr_lite/admin/designation_changes_controller.rb +33 -0
- data/app/controllers/hr_lite/admin/employees_controller.rb +53 -0
- data/app/controllers/hr_lite/admin/holidays_controller.rb +70 -0
- data/app/controllers/hr_lite/admin/leadership_controller.rb +18 -0
- data/app/controllers/hr_lite/admin/leave_balances_controller.rb +41 -0
- data/app/controllers/hr_lite/admin/leave_requests_controller.rb +41 -0
- data/app/controllers/hr_lite/admin/leave_types_controller.rb +54 -0
- data/app/controllers/hr_lite/admin/office_locations_controller.rb +46 -0
- data/app/controllers/hr_lite/admin/overview_controller.rb +12 -0
- data/app/controllers/hr_lite/admin/payroll_runs_controller.rb +93 -0
- data/app/controllers/hr_lite/admin/salary_slips_controller.rb +44 -0
- data/app/controllers/hr_lite/admin/salary_structures_controller.rb +49 -0
- data/app/controllers/hr_lite/admin/settings_controller.rb +18 -0
- data/app/controllers/hr_lite/application_controller.rb +76 -0
- data/app/controllers/hr_lite/appraisals_controller.rb +18 -0
- data/app/controllers/hr_lite/attendance_controller.rb +37 -0
- data/app/controllers/hr_lite/calendar_controller.rb +20 -0
- data/app/controllers/hr_lite/career_controller.rb +9 -0
- data/app/controllers/hr_lite/employee_profiles_controller.rb +9 -0
- data/app/controllers/hr_lite/holidays_controller.rb +9 -0
- data/app/controllers/hr_lite/home_controller.rb +7 -0
- data/app/controllers/hr_lite/kudos_controller.rb +36 -0
- data/app/controllers/hr_lite/leave_balances_controller.rb +11 -0
- data/app/controllers/hr_lite/leave_requests_controller.rb +56 -0
- data/app/controllers/hr_lite/salary_slips_controller.rb +39 -0
- data/app/controllers/hr_lite/users_controller.rb +9 -0
- data/app/helpers/hr_lite/application_helper.rb +105 -0
- data/app/jobs/hr_lite/application_job.rb +4 -0
- data/app/jobs/hr_lite/daily_digest_job.rb +33 -0
- data/app/jobs/hr_lite/leave_year_rollover_job.rb +29 -0
- data/app/mailers/hr_lite/application_mailer.rb +5 -0
- data/app/mailers/hr_lite/event_mailer.rb +33 -0
- data/app/models/concerns/hr_lite/audited.rb +70 -0
- data/app/models/concerns/hr_lite/encrypted_money.rb +32 -0
- data/app/models/hr_lite/application_record.rb +5 -0
- data/app/models/hr_lite/appraisal.rb +89 -0
- data/app/models/hr_lite/attendance_record.rb +41 -0
- data/app/models/hr_lite/audit_log.rb +17 -0
- data/app/models/hr_lite/designation_change.rb +52 -0
- data/app/models/hr_lite/employee_profile.rb +72 -0
- data/app/models/hr_lite/holiday.rb +18 -0
- data/app/models/hr_lite/kudo.rb +54 -0
- data/app/models/hr_lite/kudo_mention.rb +8 -0
- data/app/models/hr_lite/leave_balance.rb +42 -0
- data/app/models/hr_lite/leave_request.rb +207 -0
- data/app/models/hr_lite/leave_type.rb +22 -0
- data/app/models/hr_lite/office_location.rb +21 -0
- data/app/models/hr_lite/payroll_run.rb +112 -0
- data/app/models/hr_lite/salary_slip.rb +78 -0
- data/app/models/hr_lite/salary_structure.rb +49 -0
- data/app/models/hr_lite/setting.rb +14 -0
- data/app/services/hr_lite/attendance_puncher.rb +103 -0
- data/app/services/hr_lite/attendance_summary.rb +98 -0
- data/app/services/hr_lite/calculators/esi.rb +25 -0
- data/app/services/hr_lite/calculators/pf.rb +32 -0
- data/app/services/hr_lite/calculators/professional_tax.rb +20 -0
- data/app/services/hr_lite/calculators/proration.rb +30 -0
- data/app/services/hr_lite/calculators/tds.rb +74 -0
- data/app/services/hr_lite/day_status.rb +49 -0
- data/app/services/hr_lite/leave_day_counter.rb +24 -0
- data/app/services/hr_lite/overview_query.rb +39 -0
- data/app/services/hr_lite/payroll_run_processor.rb +46 -0
- data/app/services/hr_lite/pdf_renderer.rb +23 -0
- data/app/services/hr_lite/slip_builder.rb +106 -0
- data/app/services/hr_lite/working_calendar.rb +41 -0
- data/app/views/hr_lite/admin/appraisals/_form.html.erb +29 -0
- data/app/views/hr_lite/admin/appraisals/edit.html.erb +6 -0
- data/app/views/hr_lite/admin/appraisals/new.html.erb +6 -0
- data/app/views/hr_lite/admin/attendances/index.html.erb +49 -0
- data/app/views/hr_lite/admin/attendances/show.html.erb +55 -0
- data/app/views/hr_lite/admin/audit_logs/index.html.erb +39 -0
- data/app/views/hr_lite/admin/designation_changes/new.html.erb +19 -0
- data/app/views/hr_lite/admin/employees/_form.html.erb +40 -0
- data/app/views/hr_lite/admin/employees/edit.html.erb +3 -0
- data/app/views/hr_lite/admin/employees/index.html.erb +40 -0
- data/app/views/hr_lite/admin/employees/new.html.erb +3 -0
- data/app/views/hr_lite/admin/employees/show.html.erb +75 -0
- data/app/views/hr_lite/admin/holidays/index.html.erb +59 -0
- data/app/views/hr_lite/admin/leave_balances/index.html.erb +58 -0
- data/app/views/hr_lite/admin/leave_requests/index.html.erb +35 -0
- data/app/views/hr_lite/admin/leave_requests/show.html.erb +39 -0
- data/app/views/hr_lite/admin/leave_types/_form.html.erb +27 -0
- data/app/views/hr_lite/admin/leave_types/edit.html.erb +10 -0
- data/app/views/hr_lite/admin/leave_types/index.html.erb +39 -0
- data/app/views/hr_lite/admin/leave_types/new.html.erb +3 -0
- data/app/views/hr_lite/admin/office_locations/_form.html.erb +18 -0
- data/app/views/hr_lite/admin/office_locations/edit.html.erb +10 -0
- data/app/views/hr_lite/admin/office_locations/index.html.erb +30 -0
- data/app/views/hr_lite/admin/office_locations/new.html.erb +3 -0
- data/app/views/hr_lite/admin/overview/index.html.erb +72 -0
- data/app/views/hr_lite/admin/payroll_runs/index.html.erb +33 -0
- data/app/views/hr_lite/admin/payroll_runs/new.html.erb +20 -0
- data/app/views/hr_lite/admin/payroll_runs/show.html.erb +76 -0
- data/app/views/hr_lite/admin/salary_slips/show.html.erb +42 -0
- data/app/views/hr_lite/admin/salary_structures/_form.html.erb +25 -0
- data/app/views/hr_lite/admin/salary_structures/edit.html.erb +6 -0
- data/app/views/hr_lite/admin/salary_structures/new.html.erb +6 -0
- data/app/views/hr_lite/admin/settings/edit.html.erb +18 -0
- data/app/views/hr_lite/appraisals/index.html.erb +22 -0
- data/app/views/hr_lite/appraisals/show.html.erb +32 -0
- data/app/views/hr_lite/attendance/_month_grid.html.erb +41 -0
- data/app/views/hr_lite/attendance/_punch_card.html.erb +41 -0
- data/app/views/hr_lite/attendance/show.html.erb +22 -0
- data/app/views/hr_lite/calendar/show.html.erb +45 -0
- data/app/views/hr_lite/career/show.html.erb +49 -0
- data/app/views/hr_lite/employee_profiles/show.html.erb +22 -0
- data/app/views/hr_lite/event_mailer/event.html.erb +10 -0
- data/app/views/hr_lite/event_mailer/event.text.erb +10 -0
- data/app/views/hr_lite/event_mailer/leadership.html.erb +27 -0
- data/app/views/hr_lite/event_mailer/leadership.text.erb +16 -0
- data/app/views/hr_lite/holidays/_next_holiday.html.erb +12 -0
- data/app/views/hr_lite/holidays/index.html.erb +30 -0
- data/app/views/hr_lite/home/index.html.erb +25 -0
- data/app/views/hr_lite/kudos/_kudo.html.erb +17 -0
- data/app/views/hr_lite/kudos/index.html.erb +39 -0
- data/app/views/hr_lite/leave_balances/index.html.erb +19 -0
- data/app/views/hr_lite/leave_requests/_balance_chips.html.erb +17 -0
- data/app/views/hr_lite/leave_requests/index.html.erb +37 -0
- data/app/views/hr_lite/leave_requests/new.html.erb +36 -0
- data/app/views/hr_lite/leave_requests/show.html.erb +25 -0
- data/app/views/hr_lite/salary_slips/_slip_detail.html.erb +48 -0
- data/app/views/hr_lite/salary_slips/index.html.erb +29 -0
- data/app/views/hr_lite/salary_slips/pdf.html.erb +72 -0
- data/app/views/hr_lite/salary_slips/show.html.erb +12 -0
- data/app/views/hr_lite/shared/_form_errors.html.erb +10 -0
- data/app/views/hr_lite/shared/_pagination.html.erb +11 -0
- data/app/views/layouts/hr_lite/application.html.erb +56 -0
- data/app/views/layouts/hr_lite/mailer.html.erb +30 -0
- data/app/views/layouts/hr_lite/mailer.text.erb +6 -0
- data/app/views/layouts/hr_lite/pdf.html.erb +26 -0
- data/config/routes.rb +57 -0
- data/db/migrate/20260718190726_create_hr_lite_kudos.rb +14 -0
- data/db/migrate/20260718190727_create_hr_lite_kudo_mentions.rb +12 -0
- data/db/migrate/20260718190728_create_hr_lite_audit_logs.rb +15 -0
- data/db/migrate/20260718192342_create_hr_lite_office_locations.rb +13 -0
- data/db/migrate/20260718192343_create_hr_lite_attendance_records.rb +27 -0
- data/db/migrate/20260718193045_create_hr_lite_holidays.rb +14 -0
- data/db/migrate/20260718193046_create_hr_lite_leave_types.rb +20 -0
- data/db/migrate/20260718193047_create_hr_lite_leave_balances.rb +17 -0
- data/db/migrate/20260718193048_create_hr_lite_leave_requests.rb +23 -0
- data/db/migrate/20260718193049_create_hr_lite_settings.rb +10 -0
- data/db/migrate/20260718194534_create_hr_lite_employee_profiles.rb +27 -0
- data/db/migrate/20260718194535_create_hr_lite_salary_structures.rb +23 -0
- data/db/migrate/20260718194536_create_hr_lite_payroll_runs.rb +20 -0
- data/db/migrate/20260718194537_create_hr_lite_salary_slips.rb +28 -0
- data/db/migrate/20260718195953_create_hr_lite_appraisals.rb +22 -0
- data/db/migrate/20260718195954_create_hr_lite_designation_changes.rb +17 -0
- data/lib/generators/hr_lite/install/install_generator.rb +29 -0
- data/lib/generators/hr_lite/install/templates/AFTER_INSTALL +35 -0
- data/lib/generators/hr_lite/install/templates/initializer.rb +45 -0
- data/lib/hr_lite/configuration.rb +60 -0
- data/lib/hr_lite/current.rb +7 -0
- data/lib/hr_lite/engine.rb +39 -0
- data/lib/hr_lite/geo.rb +22 -0
- data/lib/hr_lite/mention_parser.rb +18 -0
- data/lib/hr_lite/money.rb +33 -0
- data/lib/hr_lite/notifications.rb +99 -0
- data/lib/hr_lite/seeds.rb +42 -0
- data/lib/hr_lite/statutory_rate_card.rb +60 -0
- data/lib/hr_lite/version.rb +3 -0
- data/lib/hr_lite.rb +104 -0
- data/lib/tasks/hr_lite_tasks.rake +8 -0
- metadata +250 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Month roll-up per user — the payroll contract. Exact math per date:
|
|
3
|
+
#
|
|
4
|
+
# holiday / weekend / present / paid full-day leave -> payable +1
|
|
5
|
+
# unpaid full-day leave (LWP) / absent -> lop +1
|
|
6
|
+
# half-day punch (no leave) -> payable +0.5, lop +0.5
|
|
7
|
+
# half-day PAID leave: leave half payable; other half payable if
|
|
8
|
+
# punched, else lop (unpaid half-day leave: leave half is lop too)
|
|
9
|
+
# future dates -> upcoming (neither)
|
|
10
|
+
# dates outside the employment window (before DOJ / after exit, when an
|
|
11
|
+
# EmployeeProfile exists) -> out_of_window (neither)
|
|
12
|
+
#
|
|
13
|
+
# The window lives HERE and only here — payroll consumes payable/lop as-is
|
|
14
|
+
# and never re-clips, so a mid-month joiner is never double-penalized.
|
|
15
|
+
# For closed months: payable + lop + upcoming + out_of_window == days_in_month.
|
|
16
|
+
module AttendanceSummary
|
|
17
|
+
def self.for(user:, month:)
|
|
18
|
+
range = month.beginning_of_month..month.end_of_month
|
|
19
|
+
profile = EmployeeProfile.find_by(user_id: user.id)
|
|
20
|
+
from_day_status(DayStatus.new(user: user, range: range), range, profile: profile)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Batch variant for team/payroll screens: 3 queries total, not 3×N.
|
|
24
|
+
def self.for_all(users:, month:)
|
|
25
|
+
users.index_with { |user| self.for(user: user, month: month) }.transform_keys(&:id)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.from_day_status(day_status, range, profile: nil)
|
|
29
|
+
summary = {
|
|
30
|
+
present: 0.to_d, half_day: 0.to_d, paid_leave: 0.to_d, unpaid_leave: 0.to_d,
|
|
31
|
+
holiday: 0, weekend: 0, absent: 0.to_d, upcoming: 0, out_of_window: 0,
|
|
32
|
+
payable_days: 0.to_d, lop_days: 0.to_d, days_in_month: range.count
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
range.each do |date|
|
|
36
|
+
if profile && !profile.active_on?(date)
|
|
37
|
+
summary[:out_of_window] += 1
|
|
38
|
+
next
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
day = day_status.for(date)
|
|
42
|
+
case day.kind
|
|
43
|
+
when :holiday
|
|
44
|
+
summary[:holiday] += 1
|
|
45
|
+
summary[:payable_days] += 1
|
|
46
|
+
when :weekend
|
|
47
|
+
summary[:weekend] += 1
|
|
48
|
+
summary[:payable_days] += 1
|
|
49
|
+
when :present
|
|
50
|
+
summary[:present] += 1
|
|
51
|
+
summary[:payable_days] += 1
|
|
52
|
+
when :half_day
|
|
53
|
+
summary[:half_day] += 1
|
|
54
|
+
summary[:payable_days] += BigDecimal("0.5")
|
|
55
|
+
summary[:lop_days] += BigDecimal("0.5")
|
|
56
|
+
when :leave
|
|
57
|
+
if day.leave.paid?
|
|
58
|
+
summary[:paid_leave] += 1
|
|
59
|
+
summary[:payable_days] += 1
|
|
60
|
+
else
|
|
61
|
+
summary[:unpaid_leave] += 1
|
|
62
|
+
summary[:lop_days] += 1
|
|
63
|
+
end
|
|
64
|
+
when :half_day_leave
|
|
65
|
+
apply_half_day_leave(summary, day)
|
|
66
|
+
when :upcoming
|
|
67
|
+
summary[:upcoming] += 1
|
|
68
|
+
else # :absent
|
|
69
|
+
summary[:absent] += 1
|
|
70
|
+
summary[:lop_days] += 1
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
summary
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.apply_half_day_leave(summary, day)
|
|
78
|
+
half = BigDecimal("0.5")
|
|
79
|
+
|
|
80
|
+
if day.leave.paid?
|
|
81
|
+
summary[:paid_leave] += half
|
|
82
|
+
summary[:payable_days] += half
|
|
83
|
+
else
|
|
84
|
+
summary[:unpaid_leave] += half
|
|
85
|
+
summary[:lop_days] += half
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if day.record&.check_in_at
|
|
89
|
+
summary[:present] += half
|
|
90
|
+
summary[:payable_days] += half
|
|
91
|
+
else
|
|
92
|
+
summary[:absent] += half
|
|
93
|
+
summary[:lop_days] += half
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
private_class_method :apply_half_day_leave
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Calculators
|
|
3
|
+
# ESI: eligibility is decided on the FULL structure monthly gross
|
|
4
|
+
# (a low-attendance month must not pull someone into ESI); when
|
|
5
|
+
# eligible, contributions apply to the earned gross and round UP
|
|
6
|
+
# to the next rupee (ESIC rule).
|
|
7
|
+
module Esi
|
|
8
|
+
Result = Struct.new(:applicable, :employee, :employer, keyword_init: true) do
|
|
9
|
+
def applicable? = applicable
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.call(monthly_gross:, gross_earned:, applicable:, rates:)
|
|
13
|
+
eligible = applicable && Money.d(monthly_gross) <= rates[:gross_ceiling]
|
|
14
|
+
return Result.new(applicable: false, employee: BigDecimal(0), employer: BigDecimal(0)) unless eligible
|
|
15
|
+
|
|
16
|
+
earned = Money.d(gross_earned)
|
|
17
|
+
Result.new(
|
|
18
|
+
applicable: true,
|
|
19
|
+
employee: Money.ceil_rupee(earned * rates[:employee_rate]),
|
|
20
|
+
employer: Money.ceil_rupee(earned * rates[:employer_rate])
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Calculators
|
|
3
|
+
# Provident fund on earned basic. Employee 12%; employer 12% split into
|
|
4
|
+
# EPS (8.33%, wage-capped) and EPF (remainder); EDLI + admin charges are
|
|
5
|
+
# employer-cost lines. All contributions round to the nearest rupee.
|
|
6
|
+
module Pf
|
|
7
|
+
Result = Struct.new(:pf_wage, :employee, :employer_eps, :employer_epf,
|
|
8
|
+
:edli, :admin_charges, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
def self.call(basic_earned:, on_full_basic:, rates:)
|
|
11
|
+
basic = Money.d(basic_earned)
|
|
12
|
+
pf_wage = on_full_basic ? basic : [ basic, rates[:wage_ceiling] ].min
|
|
13
|
+
|
|
14
|
+
employee = Money.round_rupee(pf_wage * rates[:employee_rate])
|
|
15
|
+
employer_total = Money.round_rupee(pf_wage * rates[:employer_rate])
|
|
16
|
+
eps_wage = [ pf_wage, rates[:eps_wage_ceiling] ].min
|
|
17
|
+
eps = Money.round_rupee(eps_wage * rates[:eps_rate])
|
|
18
|
+
eps = employer_total if eps > employer_total
|
|
19
|
+
edli_wage = [ pf_wage, rates[:edli_ceiling] ].min
|
|
20
|
+
|
|
21
|
+
Result.new(
|
|
22
|
+
pf_wage: pf_wage,
|
|
23
|
+
employee: employee,
|
|
24
|
+
employer_eps: eps,
|
|
25
|
+
employer_epf: employer_total - eps,
|
|
26
|
+
edli: Money.round_rupee(edli_wage * rates[:edli_rate]),
|
|
27
|
+
admin_charges: Money.round_rupee(pf_wage * rates[:admin_rate])
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Calculators
|
|
3
|
+
# State-slab professional tax on earned gross. Unknown states and
|
|
4
|
+
# states without PT (UP, Uttarakhand) simply yield zero. Slabs may
|
|
5
|
+
# carry a feb_extra for states that top up the February deduction.
|
|
6
|
+
module ProfessionalTax
|
|
7
|
+
def self.call(state:, gross_earned:, period_month:, rates:)
|
|
8
|
+
slabs = rates[state.to_s] || []
|
|
9
|
+
earned = Money.d(gross_earned)
|
|
10
|
+
|
|
11
|
+
slab = slabs.reverse.find { |row| earned > row[:above] }
|
|
12
|
+
return BigDecimal(0) unless slab
|
|
13
|
+
|
|
14
|
+
amount = slab[:monthly]
|
|
15
|
+
amount += slab[:feb_extra] if slab[:feb_extra] && period_month.month == 2
|
|
16
|
+
Money.round_rupee(amount)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Calculators
|
|
3
|
+
# Calendar-day proration: each structure component earns
|
|
4
|
+
# full × payable/days_in_month, rounded to paise per line.
|
|
5
|
+
module Proration
|
|
6
|
+
COMPONENTS = [
|
|
7
|
+
[ "basic", "Basic" ],
|
|
8
|
+
[ "hra", "HRA" ],
|
|
9
|
+
[ "special_allowance", "Special allowance" ],
|
|
10
|
+
[ "other_earnings", "Other earnings" ]
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
def self.call(structure:, payable_days:, days_in_month:)
|
|
14
|
+
factor = Money.d(payable_days) / Money.d(days_in_month)
|
|
15
|
+
|
|
16
|
+
COMPONENTS.filter_map do |attribute, label|
|
|
17
|
+
full = structure.public_send(attribute)
|
|
18
|
+
next if full.nil? || full.zero?
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
code: attribute,
|
|
22
|
+
label: label,
|
|
23
|
+
full_amount: Money.round2(full),
|
|
24
|
+
amount: Money.round2(full * factor)
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Calculators
|
|
3
|
+
# Projection-grade TDS (not Form-16-grade — no surcharge, perquisites or
|
|
4
|
+
# HRA-exemption math; the per-slip override is the escape hatch and the
|
|
5
|
+
# slip shows the whole working so "why is my TDS X" answers itself).
|
|
6
|
+
#
|
|
7
|
+
# projected annual gross = FY gross paid so far + this month
|
|
8
|
+
# + structure gross × months remaining after this one
|
|
9
|
+
# taxable = projected − standard deduction − (old regime: declared deductions)
|
|
10
|
+
# annual = slab tax − 87A rebate (full when taxable ≤ cap) + 4% cess, §288B rounded
|
|
11
|
+
# monthly = max((annual − TDS already deducted) / months remaining incl. this, 0)
|
|
12
|
+
module Tds
|
|
13
|
+
Result = Struct.new(:monthly, :projected_annual_gross, :taxable, :annual_tax,
|
|
14
|
+
:details, keyword_init: true)
|
|
15
|
+
|
|
16
|
+
HIGH_INCOME_WARNING_THRESHOLD = BigDecimal("5000000")
|
|
17
|
+
|
|
18
|
+
def self.call(regime:, structure_monthly_gross:, gross_earned_this_month:,
|
|
19
|
+
fy_gross_paid:, fy_tds_paid:, months_remaining:,
|
|
20
|
+
declared_annual_deductions:, rates:, override: nil)
|
|
21
|
+
table = rates[regime.to_s] || rates["new"]
|
|
22
|
+
|
|
23
|
+
if override.present?
|
|
24
|
+
monthly = Money.round_rupee(override)
|
|
25
|
+
return Result.new(monthly: monthly, projected_annual_gross: nil, taxable: nil, annual_tax: nil,
|
|
26
|
+
details: { "regime" => regime.to_s, "override" => monthly.to_s("F") })
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
months_remaining = [ months_remaining.to_i, 1 ].max
|
|
30
|
+
projected = Money.d(fy_gross_paid) + Money.d(gross_earned_this_month) +
|
|
31
|
+
(Money.d(structure_monthly_gross) * (months_remaining - 1))
|
|
32
|
+
|
|
33
|
+
deductions = table[:standard_deduction]
|
|
34
|
+
deductions += Money.d(declared_annual_deductions) if regime.to_s == "old"
|
|
35
|
+
taxable = [ projected - deductions, BigDecimal(0) ].max
|
|
36
|
+
|
|
37
|
+
slab_tax = slab_tax_for(taxable, table[:slabs])
|
|
38
|
+
slab_tax = BigDecimal(0) if taxable <= table[:rebate_cap] # §87A full rebate
|
|
39
|
+
annual = Money.round_to_10(slab_tax * (1 + table[:cess_rate]))
|
|
40
|
+
|
|
41
|
+
monthly = Money.round_rupee((annual - Money.d(fy_tds_paid)) / months_remaining)
|
|
42
|
+
monthly = BigDecimal(0) if monthly.negative?
|
|
43
|
+
|
|
44
|
+
Result.new(
|
|
45
|
+
monthly: monthly,
|
|
46
|
+
projected_annual_gross: Money.round2(projected),
|
|
47
|
+
taxable: Money.round2(taxable),
|
|
48
|
+
annual_tax: annual,
|
|
49
|
+
details: {
|
|
50
|
+
"regime" => regime.to_s,
|
|
51
|
+
"projected_annual_gross" => Money.round2(projected).to_s("F"),
|
|
52
|
+
"standard_deduction" => table[:standard_deduction].to_s("F"),
|
|
53
|
+
"declared_deductions" => (regime.to_s == "old" ? Money.d(declared_annual_deductions).to_s("F") : "0"),
|
|
54
|
+
"taxable" => Money.round2(taxable).to_s("F"),
|
|
55
|
+
"annual_tax_with_cess" => annual.to_s("F"),
|
|
56
|
+
"fy_tds_already_deducted" => Money.d(fy_tds_paid).to_s("F"),
|
|
57
|
+
"months_remaining" => months_remaining.to_s,
|
|
58
|
+
"high_income_review" => (taxable > HIGH_INCOME_WARNING_THRESHOLD).to_s
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.slab_tax_for(taxable, slabs)
|
|
64
|
+
slabs.sum(BigDecimal(0)) do |lower, upper, rate|
|
|
65
|
+
next BigDecimal(0) if taxable <= lower
|
|
66
|
+
|
|
67
|
+
span_top = upper && taxable > upper ? upper : taxable
|
|
68
|
+
(span_top - lower) * rate
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
private_class_method :slab_tax_for
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Resolves what each date "is" for a user, with the full precedence chain:
|
|
3
|
+
#
|
|
4
|
+
# holiday > weekend > approved full-day leave > punch > absent/upcoming
|
|
5
|
+
#
|
|
6
|
+
# A half-day leave coexists with a punch (kind :half_day_leave, record
|
|
7
|
+
# exposed); a punch on a holiday keeps :holiday but exposes the record
|
|
8
|
+
# (UI shows a "worked" hint). One resolver feeds the month grids, the
|
|
9
|
+
# overview board and the attendance summary — a single truth.
|
|
10
|
+
class DayStatus
|
|
11
|
+
Day = Struct.new(:kind, :record, :leave, keyword_init: true)
|
|
12
|
+
|
|
13
|
+
def initialize(user:, range:)
|
|
14
|
+
@range = range
|
|
15
|
+
@records = AttendanceRecord.where(user_id: user.id, date: range).index_by(&:date)
|
|
16
|
+
@calendar = WorkingCalendar.new(range)
|
|
17
|
+
@leaves = LeaveRequest.approved.includes(:leave_type)
|
|
18
|
+
.where(user_id: user.id)
|
|
19
|
+
.overlapping_range(range.first, range.last).to_a
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :calendar
|
|
23
|
+
|
|
24
|
+
def for(date)
|
|
25
|
+
record = @records[date]
|
|
26
|
+
leave = @leaves.find { |l| l.start_date <= date && l.end_date >= date }
|
|
27
|
+
|
|
28
|
+
return Day.new(kind: :holiday, record: record, leave: leave) if @calendar.holiday?(date)
|
|
29
|
+
return Day.new(kind: :weekend, record: record, leave: leave) if @calendar.weekend?(date)
|
|
30
|
+
|
|
31
|
+
if leave
|
|
32
|
+
kind = leave.half_day ? :half_day_leave : :leave
|
|
33
|
+
return Day.new(kind: kind, record: record, leave: leave)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if record&.check_in_at
|
|
37
|
+
Day.new(kind: record.status.to_sym, record: record)
|
|
38
|
+
elsif date > Date.current
|
|
39
|
+
Day.new(kind: :upcoming, record: nil)
|
|
40
|
+
else
|
|
41
|
+
Day.new(kind: :absent, record: record)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def counts
|
|
46
|
+
@range.each_with_object(Hash.new(0)) { |date, acc| acc[self.for(date).kind] += 1 }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# How many working days a leave request actually consumes. Weekends and
|
|
3
|
+
# company-wide holidays inside the range are free; a half-day (single-day
|
|
4
|
+
# requests only) consumes 0.5.
|
|
5
|
+
module LeaveDayCounter
|
|
6
|
+
def self.count(leave_request)
|
|
7
|
+
count_range(
|
|
8
|
+
start_date: leave_request.start_date,
|
|
9
|
+
end_date: leave_request.end_date,
|
|
10
|
+
half_day: leave_request.half_day
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.count_range(start_date:, end_date:, half_day: false)
|
|
15
|
+
return 0 if start_date.nil? || end_date.nil? || end_date < start_date
|
|
16
|
+
|
|
17
|
+
range = start_date..end_date
|
|
18
|
+
working = WorkingCalendar.new(range).working_days_in(range)
|
|
19
|
+
return BigDecimal("0.5") if half_day && working.positive?
|
|
20
|
+
|
|
21
|
+
BigDecimal(working)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Feeds both the admin overview board and the daily leadership digest —
|
|
3
|
+
# one source, so they always agree. Sections return Relations (callers
|
|
4
|
+
# paginate or cap as they see fit).
|
|
5
|
+
class OverviewQuery
|
|
6
|
+
def initialize(date: Date.current)
|
|
7
|
+
@date = date
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def pending_requests
|
|
11
|
+
LeaveRequest.pending.includes(:leave_type, :user).order(:created_at)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def on_leave_today
|
|
15
|
+
LeaveRequest.active_on(@date).includes(:leave_type, :user).order(:start_date)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def flagged_today
|
|
19
|
+
AttendanceRecord.for_date(@date).flagged.includes(:user)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def missing_checkout_yesterday
|
|
23
|
+
AttendanceRecord.for_date(@date - 1).missing_checkout.includes(:user)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def kpis
|
|
27
|
+
{
|
|
28
|
+
pending: pending_requests.count,
|
|
29
|
+
on_leave: on_leave_today.count,
|
|
30
|
+
flagged: flagged_today.count,
|
|
31
|
+
missing_checkout: missing_checkout_yesterday.count
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def empty?
|
|
36
|
+
kpis.values.all?(&:zero?)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# (Re)computes every slip for a run in one transaction. Missing structures
|
|
3
|
+
# become warnings, manual overrides survive recomputes, employees who
|
|
4
|
+
# became ineligible lose their draft slips.
|
|
5
|
+
class PayrollRunProcessor
|
|
6
|
+
def self.call(run)
|
|
7
|
+
new(run).call
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(run)
|
|
11
|
+
@run = run
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
warnings = []
|
|
16
|
+
|
|
17
|
+
ActiveRecord::Base.transaction do
|
|
18
|
+
eligible = EmployeeProfile.active_for(@run.period_month).includes(:user).to_a
|
|
19
|
+
keep_ids = []
|
|
20
|
+
|
|
21
|
+
eligible.each do |profile|
|
|
22
|
+
user = profile.user
|
|
23
|
+
structure = SalaryStructure.effective_for(user, @run.period_month)
|
|
24
|
+
if structure.nil?
|
|
25
|
+
warnings << "No salary structure for #{HrLite.display_name(user)} — skipped"
|
|
26
|
+
next
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
slip = @run.salary_slips.find_or_initialize_by(user_id: user.id)
|
|
30
|
+
attributes = SlipBuilder.call(
|
|
31
|
+
run: @run, user: user, structure: structure, profile: profile,
|
|
32
|
+
lop_override: slip.lop_override, tds_override: slip.tds_override
|
|
33
|
+
)
|
|
34
|
+
slip.assign_attributes(attributes)
|
|
35
|
+
slip.save!
|
|
36
|
+
keep_ids << slip.id
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@run.salary_slips.where.not(id: keep_ids).destroy_all
|
|
40
|
+
@run.update!(warnings: warnings)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@run
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Slip PDF entry point. Host override first (earthly_vms plugs its cached
|
|
3
|
+
# WickedPdf service in via config.render_pdf); otherwise a built-in
|
|
4
|
+
# WickedPdf render when the gem is present; otherwise nil — the controller
|
|
5
|
+
# explains that PDF is not configured rather than crashing.
|
|
6
|
+
module PdfRenderer
|
|
7
|
+
def self.render(template:, assigns:, cache_key: nil)
|
|
8
|
+
if HrLite.config.render_pdf
|
|
9
|
+
HrLite.config.render_pdf.call(template: template, assigns: assigns, cache_key: cache_key)
|
|
10
|
+
elsif wicked_pdf_class
|
|
11
|
+
html = HrLite::ApplicationController.render(
|
|
12
|
+
template: template, assigns: assigns, layout: "hr_lite/pdf", formats: [ :html ]
|
|
13
|
+
)
|
|
14
|
+
wicked_pdf_class.new.pdf_from_string(html, page_size: "A4")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Seam for tests and for hosts that lazy-load wicked_pdf.
|
|
19
|
+
def self.wicked_pdf_class
|
|
20
|
+
defined?(WickedPdf) ? WickedPdf : nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Assembles one employee's slip numbers for a run. Sequence: attendance
|
|
3
|
+
# summary -> proration -> PF -> ESI -> PT -> TDS -> totals. Every line is
|
|
4
|
+
# rounded by its own statutory rule exactly once; net pay is a plain
|
|
5
|
+
# subtraction of already-rounded lines, so nothing can drift.
|
|
6
|
+
class SlipBuilder
|
|
7
|
+
def self.call(run:, user:, structure:, profile:, lop_override: nil, tds_override: nil)
|
|
8
|
+
new(run, user, structure, profile, lop_override, tds_override).call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(run, user, structure, profile, lop_override, tds_override)
|
|
12
|
+
@run = run
|
|
13
|
+
@user = user
|
|
14
|
+
@structure = structure
|
|
15
|
+
@profile = profile
|
|
16
|
+
@lop_override = lop_override
|
|
17
|
+
@tds_override = tds_override
|
|
18
|
+
@rates = StatutoryRateCard.for(run.period_month)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call
|
|
22
|
+
summary = AttendanceSummary.for(user: @user, month: @run.period_month)
|
|
23
|
+
days_in_month = summary[:days_in_month]
|
|
24
|
+
lop = Money.d(@lop_override || summary[:lop_days])
|
|
25
|
+
payable = [ Money.d(days_in_month) - Money.d(summary[:out_of_window]) - lop, Money.d(0) ].max
|
|
26
|
+
|
|
27
|
+
earnings = Calculators::Proration.call(
|
|
28
|
+
structure: @structure, payable_days: payable, days_in_month: days_in_month
|
|
29
|
+
)
|
|
30
|
+
gross_earned = earnings.sum(BigDecimal(0)) { |row| row[:amount] }
|
|
31
|
+
basic_earned = earnings.find { |row| row[:code] == "basic" }&.fetch(:amount) || BigDecimal(0)
|
|
32
|
+
|
|
33
|
+
deductions = []
|
|
34
|
+
employer_costs = {}
|
|
35
|
+
|
|
36
|
+
if @structure.pf_applicable
|
|
37
|
+
pf = Calculators::Pf.call(basic_earned: basic_earned,
|
|
38
|
+
on_full_basic: @structure.pf_on_full_basic, rates: @rates[:pf])
|
|
39
|
+
deductions << { code: "pf_employee", label: "Provident fund", amount: pf.employee,
|
|
40
|
+
meta: { pf_wage: pf.pf_wage.to_s("F") } }
|
|
41
|
+
employer_costs.merge!(
|
|
42
|
+
pf_eps: pf.employer_eps, pf_epf: pf.employer_epf,
|
|
43
|
+
edli: pf.edli, pf_admin: pf.admin_charges
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
esi = Calculators::Esi.call(monthly_gross: @structure.monthly_gross, gross_earned: gross_earned,
|
|
48
|
+
applicable: @structure.esi_applicable, rates: @rates[:esi])
|
|
49
|
+
if esi.applicable?
|
|
50
|
+
deductions << { code: "esi_employee", label: "ESI", amount: esi.employee }
|
|
51
|
+
employer_costs[:esi_employer] = esi.employer
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
pt = Calculators::ProfessionalTax.call(state: @structure.pt_state, gross_earned: gross_earned,
|
|
55
|
+
period_month: @run.period_month, rates: @rates[:pt])
|
|
56
|
+
deductions << { code: "pt", label: "Professional tax", amount: pt } if pt.positive?
|
|
57
|
+
|
|
58
|
+
fy = SalarySlip.fy_to_date(@user, @run.period_month)
|
|
59
|
+
tds = Calculators::Tds.call(
|
|
60
|
+
regime: @profile.tax_regime,
|
|
61
|
+
structure_monthly_gross: @structure.monthly_gross,
|
|
62
|
+
gross_earned_this_month: gross_earned,
|
|
63
|
+
fy_gross_paid: fy[:gross],
|
|
64
|
+
fy_tds_paid: fy[:tds],
|
|
65
|
+
months_remaining: months_remaining_in_fy,
|
|
66
|
+
declared_annual_deductions: @profile.declared_annual_deductions,
|
|
67
|
+
rates: @rates[:income_tax],
|
|
68
|
+
override: @tds_override
|
|
69
|
+
)
|
|
70
|
+
deductions << { code: "tds", label: "Income tax (TDS)", amount: tds.monthly } if tds.monthly.positive?
|
|
71
|
+
|
|
72
|
+
total_deductions = deductions.sum(BigDecimal(0)) { |row| row[:amount] }
|
|
73
|
+
|
|
74
|
+
{
|
|
75
|
+
period_month: @run.period_month,
|
|
76
|
+
days_in_month: days_in_month,
|
|
77
|
+
payable_days: payable,
|
|
78
|
+
lop_days: Money.d(summary[:lop_days]),
|
|
79
|
+
lop_override: @lop_override,
|
|
80
|
+
tds_override: @tds_override,
|
|
81
|
+
earnings: serialize_rows(earnings),
|
|
82
|
+
deductions: serialize_rows(deductions),
|
|
83
|
+
employer_costs: employer_costs.transform_values { |v| v.to_s("F") }.to_json,
|
|
84
|
+
tax_details: tds.details.to_json,
|
|
85
|
+
gross_earnings: gross_earned,
|
|
86
|
+
total_deductions: total_deductions,
|
|
87
|
+
net_pay: gross_earned - total_deductions,
|
|
88
|
+
computed_at: Time.current
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
# Months left in the Indian FY including the run month itself.
|
|
95
|
+
def months_remaining_in_fy
|
|
96
|
+
month = @run.period_month.month
|
|
97
|
+
month >= 4 ? (15 - month) : (3 - month + 1)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def serialize_rows(rows)
|
|
101
|
+
rows.map { |row|
|
|
102
|
+
row.transform_values { |value| value.is_a?(BigDecimal) ? value.to_s("F") : value }
|
|
103
|
+
}.to_json
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# The single source of day classification. Preloads holidays and the
|
|
3
|
+
# weekend policy once per instance; every other service (leave counting,
|
|
4
|
+
# day status, attendance summary) delegates here.
|
|
5
|
+
class WorkingCalendar
|
|
6
|
+
def initialize(range)
|
|
7
|
+
@range = range
|
|
8
|
+
@holiday_dates = Holiday.dates_for(range)
|
|
9
|
+
@policy = Setting.instance.weekend_policy
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def holiday?(date)
|
|
13
|
+
@holiday_dates.include?(date)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def weekend?(date)
|
|
17
|
+
case @policy
|
|
18
|
+
when "sun_only"
|
|
19
|
+
date.sunday?
|
|
20
|
+
when "second_fourth_sat_sun"
|
|
21
|
+
date.sunday? || (date.saturday? && [ 2, 4 ].include?(week_of_month(date)))
|
|
22
|
+
else # sat_sun
|
|
23
|
+
date.saturday? || date.sunday?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def working_day?(date)
|
|
28
|
+
!holiday?(date) && !weekend?(date)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def working_days_in(range)
|
|
32
|
+
range.count { |date| working_day?(date) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def week_of_month(date)
|
|
38
|
+
((date.day - 1) / 7) + 1
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<section class="hrl-card">
|
|
2
|
+
<%= form_with model: appraisal, url: url, method: method, scope: :appraisal, local: true do |f| %>
|
|
3
|
+
<%= render "hr_lite/shared/form_errors", record: appraisal %>
|
|
4
|
+
<div class="hrl-field"><%= f.label :period_start %><%= f.date_field :period_start %></div>
|
|
5
|
+
<div class="hrl-field"><%= f.label :period_end %><%= f.date_field :period_end %></div>
|
|
6
|
+
<div class="hrl-field">
|
|
7
|
+
<%= f.label :rating, "Rating (1-5, optional)" %>
|
|
8
|
+
<%= f.select :rating, [ [ "No rating", "" ] ] + (1..5).map { |n| [ "#{n}/5", n ] } %>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="hrl-field"><%= f.label :strengths %><%= f.text_area :strengths %></div>
|
|
11
|
+
<div class="hrl-field"><%= f.label :improvements, "Areas to grow" %><%= f.text_area :improvements %></div>
|
|
12
|
+
<div class="hrl-field">
|
|
13
|
+
<%= f.label :outcome %>
|
|
14
|
+
<%= f.select :outcome, HrLite::Appraisal::OUTCOMES.map { |o| [ o.humanize, o ] } %>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="hrl-field">
|
|
17
|
+
<%= f.label :new_designation, "New designation (promotion only)" %>
|
|
18
|
+
<%= f.text_field :new_designation %>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="hrl-field">
|
|
21
|
+
<%= f.label :effective_date, "Effective date (for increment/promotion)" %>
|
|
22
|
+
<%= f.date_field :effective_date %>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="hrl-form-actions">
|
|
25
|
+
<%= f.submit "Save draft", class: "hrl-btn hrl-btn--primary" %>
|
|
26
|
+
<a class="hrl-btn" href="<%= hr_lite.admin_employee_path(@profile) %>">Cancel</a>
|
|
27
|
+
</div>
|
|
28
|
+
<% end %>
|
|
29
|
+
</section>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Edit appraisal" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Edit appraisal — <%= hr_display_name(@profile.user) %></h1>
|
|
4
|
+
</div>
|
|
5
|
+
<%= render "form", appraisal: @appraisal,
|
|
6
|
+
url: hr_lite.admin_employee_appraisal_path(@profile, @appraisal), method: :patch %>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<% content_for(:page_title) { "New appraisal" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">New appraisal — <%= hr_display_name(@profile.user) %></h1>
|
|
4
|
+
</div>
|
|
5
|
+
<%= render "form", appraisal: @appraisal,
|
|
6
|
+
url: hr_lite.admin_employee_appraisals_path(@profile), method: :post %>
|