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,74 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
class AppraisalsController < LeadershipController
|
|
4
|
+
before_action :set_profile
|
|
5
|
+
|
|
6
|
+
def new
|
|
7
|
+
@appraisal = Appraisal.new(
|
|
8
|
+
user_id: @profile.user_id,
|
|
9
|
+
period_start: Date.current.beginning_of_year, period_end: Date.current
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create
|
|
14
|
+
@appraisal = Appraisal.new(appraisal_params.merge(
|
|
15
|
+
user_id: @profile.user_id, reviewer_id: hr_current_user.id
|
|
16
|
+
))
|
|
17
|
+
if @appraisal.save
|
|
18
|
+
redirect_to admin_employee_path(@profile), notice: "Appraisal drafted — share when ready."
|
|
19
|
+
else
|
|
20
|
+
render :new, status: :unprocessable_entity
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def edit
|
|
25
|
+
@appraisal = scoped.find(params[:id])
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def update
|
|
29
|
+
@appraisal = scoped.find(params[:id])
|
|
30
|
+
if @appraisal.update(appraisal_params)
|
|
31
|
+
redirect_to admin_employee_path(@profile), notice: "Appraisal updated."
|
|
32
|
+
else
|
|
33
|
+
render :edit, status: :unprocessable_entity
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def share
|
|
38
|
+
appraisal = scoped.find(params[:id])
|
|
39
|
+
appraisal.share!(actor: hr_current_user)
|
|
40
|
+
redirect_to admin_employee_path(@profile), notice: "Appraisal shared with the employee."
|
|
41
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
42
|
+
message = e.record.errors.full_messages.to_sentence.presence || "This appraisal was already shared."
|
|
43
|
+
redirect_to admin_employee_path(@profile), alert: message
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def destroy
|
|
47
|
+
appraisal = scoped.find(params[:id])
|
|
48
|
+
if appraisal.destroy
|
|
49
|
+
redirect_to admin_employee_path(@profile), notice: "Draft appraisal removed.", status: :see_other
|
|
50
|
+
else
|
|
51
|
+
redirect_to admin_employee_path(@profile), status: :see_other,
|
|
52
|
+
alert: appraisal.errors.full_messages.to_sentence
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def set_profile
|
|
59
|
+
@profile = EmployeeProfile.find(params[:employee_id])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def scoped
|
|
63
|
+
Appraisal.where(user_id: @profile.user_id)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def appraisal_params
|
|
67
|
+
params.require(:appraisal).permit(
|
|
68
|
+
:period_start, :period_end, :rating, :strengths, :improvements,
|
|
69
|
+
:outcome, :effective_date, :new_designation
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
class AttendancesController < BaseController
|
|
4
|
+
# Team day view: everyone × their punch/status for one date.
|
|
5
|
+
def index
|
|
6
|
+
@date = parse_date_param(params[:date])
|
|
7
|
+
@employees = HrLite.employees
|
|
8
|
+
@records = AttendanceRecord.for_date(@date).where(user_id: @employees.map(&:id)).index_by(&:user_id)
|
|
9
|
+
@flagged_count = @records.values.count(&:flagged?)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# One employee's month + the regularization form for ?date=.
|
|
13
|
+
def show
|
|
14
|
+
@employee = HrLite.user_klass.find(params[:user_id])
|
|
15
|
+
@month = parse_month_param(params[:month])
|
|
16
|
+
@day_status = DayStatus.new(user: @employee, range: @month.beginning_of_month..@month.end_of_month)
|
|
17
|
+
@counts = @day_status.counts
|
|
18
|
+
@edit_date = params[:date].present? ? parse_date_param(params[:date]) : nil
|
|
19
|
+
@edit_record = @edit_date && AttendanceRecord.find_or_initialize_by(user_id: @employee.id, date: @edit_date)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Regularization: fix punches with a mandatory note, fully audited.
|
|
23
|
+
# Clearing both punch times deletes the record (that is how an
|
|
24
|
+
# erroneous punch is removed).
|
|
25
|
+
def update
|
|
26
|
+
@employee = HrLite.user_klass.find(params[:user_id])
|
|
27
|
+
date = parse_date_param(params[:date])
|
|
28
|
+
record = AttendanceRecord.find_or_initialize_by(user_id: @employee.id, date: date)
|
|
29
|
+
|
|
30
|
+
note = params.dig(:attendance_record, :regularization_note).to_s.strip
|
|
31
|
+
if note.blank?
|
|
32
|
+
return redirect_to admin_attendance_path(@employee.id, date: date, month: date.strftime("%Y-%m")),
|
|
33
|
+
alert: "A regularization note is required."
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attrs = params.require(:attendance_record).permit(:check_in_at, :check_out_at, :status)
|
|
37
|
+
if attrs[:check_in_at].blank? && attrs[:check_out_at].blank?
|
|
38
|
+
record.destroy if record.persisted?
|
|
39
|
+
log_regularization(record, note, removed: true)
|
|
40
|
+
return redirect_to admin_attendance_path(@employee.id, month: date.strftime("%Y-%m")),
|
|
41
|
+
notice: "Punch removed."
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
record.assign_attributes(attrs)
|
|
45
|
+
record.status = "present" if record.status.blank?
|
|
46
|
+
record.regularized_by_id = hr_current_user.id
|
|
47
|
+
record.regularized_at = Time.current
|
|
48
|
+
record.regularization_note = note
|
|
49
|
+
|
|
50
|
+
if record.save
|
|
51
|
+
log_regularization(record, note, removed: false)
|
|
52
|
+
redirect_to admin_attendance_path(@employee.id, month: date.strftime("%Y-%m")),
|
|
53
|
+
notice: "Attendance updated."
|
|
54
|
+
else
|
|
55
|
+
redirect_to admin_attendance_path(@employee.id, date: date, month: date.strftime("%Y-%m")),
|
|
56
|
+
alert: record.errors.full_messages.to_sentence
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def log_regularization(record, note, removed:)
|
|
63
|
+
AuditLog.create!(
|
|
64
|
+
actor: hr_current_user, action: removed ? "destroy" : "regularize",
|
|
65
|
+
subject_type: record.class.name, subject_id: record.id || 0,
|
|
66
|
+
audited_changes: { "date" => record.date.to_s, "note" => note }
|
|
67
|
+
)
|
|
68
|
+
Notifications.publish(
|
|
69
|
+
"attendance.regularized",
|
|
70
|
+
title: "Attendance #{removed ? 'punch removed' : 'regularized'} for #{record.date.strftime('%d %b')}",
|
|
71
|
+
body: note,
|
|
72
|
+
path: "/attendance?month=#{record.date.strftime('%Y-%m')}",
|
|
73
|
+
bell_to: [ record.user ],
|
|
74
|
+
email_to: [ record.user ]
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
# Day-to-day operations tier: overview board, team attendance,
|
|
4
|
+
# leave decisions, balance adjustments. Leadership members are
|
|
5
|
+
# allowed too — governing implies at least operational visibility.
|
|
6
|
+
class BaseController < ApplicationController
|
|
7
|
+
before_action :require_hr_admin!
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def require_hr_admin!
|
|
12
|
+
hr_access_denied unless hr_admin? || hr_leadership?
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
# Standalone role change (promotion without an appraisal).
|
|
4
|
+
class DesignationChangesController < LeadershipController
|
|
5
|
+
before_action :set_profile
|
|
6
|
+
|
|
7
|
+
def new
|
|
8
|
+
@change = DesignationChange.new(user_id: @profile.user_id, effective_date: Date.current)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def create
|
|
12
|
+
@change = DesignationChange.new(change_params.merge(
|
|
13
|
+
user_id: @profile.user_id, created_by_id: hr_current_user.id
|
|
14
|
+
))
|
|
15
|
+
if @change.save
|
|
16
|
+
redirect_to admin_employee_path(@profile), notice: "Role change recorded."
|
|
17
|
+
else
|
|
18
|
+
render :new, status: :unprocessable_entity
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def set_profile
|
|
25
|
+
@profile = EmployeeProfile.find(params[:employee_id])
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def change_params
|
|
29
|
+
params.require(:designation_change).permit(:to_designation, :effective_date, :note)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
# Employee HR profiles (leadership). No destroy — exits are a date;
|
|
4
|
+
# payroll history is a statutory record.
|
|
5
|
+
class EmployeesController < LeadershipController
|
|
6
|
+
def index
|
|
7
|
+
@profiles = paginate(EmployeeProfile.includes(:user).order(:employee_code))
|
|
8
|
+
@users_without_profile = HrLite.employees.reject { |u| EmployeeProfile.exists?(user_id: u.id) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def show
|
|
12
|
+
@profile = EmployeeProfile.includes(:user).find(params[:id])
|
|
13
|
+
@structures = SalaryStructure.where(user_id: @profile.user_id).order(effective_from: :desc)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def new
|
|
17
|
+
@profile = EmployeeProfile.new(user_id: params[:user_id], date_of_joining: Date.current)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create
|
|
21
|
+
@profile = EmployeeProfile.new(profile_params)
|
|
22
|
+
if @profile.save
|
|
23
|
+
redirect_to admin_employee_path(@profile), notice: "Employee profile created."
|
|
24
|
+
else
|
|
25
|
+
render :new, status: :unprocessable_entity
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def edit
|
|
30
|
+
@profile = EmployeeProfile.find(params[:id])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def update
|
|
34
|
+
@profile = EmployeeProfile.find(params[:id])
|
|
35
|
+
if @profile.update(profile_params)
|
|
36
|
+
redirect_to admin_employee_path(@profile), notice: "Employee profile updated."
|
|
37
|
+
else
|
|
38
|
+
render :edit, status: :unprocessable_entity
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def profile_params
|
|
45
|
+
params.require(:employee_profile).permit(
|
|
46
|
+
:user_id, :employee_code, :designation, :date_of_birth, :date_of_joining,
|
|
47
|
+
:date_of_exit, :department, :work_location, :pan_number, :pf_uan, :esi_number,
|
|
48
|
+
:bank_account_number, :bank_ifsc, :bank_name, :tax_regime, :declared_annual_deductions
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
class HolidaysController < LeadershipController
|
|
4
|
+
def index
|
|
5
|
+
year = params[:year].to_i
|
|
6
|
+
@year = year.between?(2000, 2100) ? year : Date.current.year
|
|
7
|
+
@holidays = Holiday.where(date: Date.new(@year, 1, 1)..Date.new(@year, 12, 31)).order(:date)
|
|
8
|
+
@holiday = Holiday.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def create
|
|
12
|
+
@holiday = Holiday.new(holiday_params)
|
|
13
|
+
if @holiday.save
|
|
14
|
+
redirect_to admin_holidays_path(year: @holiday.date.year), notice: "Holiday added."
|
|
15
|
+
else
|
|
16
|
+
redirect_to admin_holidays_path, alert: @holiday.errors.full_messages.to_sentence
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def update
|
|
21
|
+
holiday = Holiday.find(params[:id])
|
|
22
|
+
if holiday.update(holiday_params)
|
|
23
|
+
redirect_to admin_holidays_path(year: holiday.date.year), notice: "Holiday updated."
|
|
24
|
+
else
|
|
25
|
+
redirect_to admin_holidays_path, alert: holiday.errors.full_messages.to_sentence
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def destroy
|
|
30
|
+
holiday = Holiday.find(params[:id])
|
|
31
|
+
holiday.destroy!
|
|
32
|
+
redirect_to admin_holidays_path(year: holiday.date.year),
|
|
33
|
+
notice: "Holiday removed.", status: :see_other
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Bulk paste: one "YYYY-MM-DD, Name[, optional]" per line. Reports
|
|
37
|
+
# per-line problems, skips duplicates silently.
|
|
38
|
+
def bulk_create
|
|
39
|
+
created = 0
|
|
40
|
+
problems = []
|
|
41
|
+
|
|
42
|
+
params[:lines].to_s.each_line.with_index(1) do |line, number|
|
|
43
|
+
line = line.strip
|
|
44
|
+
next if line.blank?
|
|
45
|
+
|
|
46
|
+
date_part, name_part, flag = line.split(",", 3).map { |part| part.to_s.strip }
|
|
47
|
+
date = Date.strptime(date_part.to_s, "%Y-%m-%d") rescue nil
|
|
48
|
+
if date.nil? || name_part.blank?
|
|
49
|
+
problems << "Line #{number}: expected YYYY-MM-DD, Name"
|
|
50
|
+
next
|
|
51
|
+
end
|
|
52
|
+
next if Holiday.exists?(date: date)
|
|
53
|
+
|
|
54
|
+
Holiday.create!(date: date, name: name_part, optional: flag.to_s.casecmp?("optional"))
|
|
55
|
+
created += 1
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
message = "#{created} holiday#{'s' unless created == 1} added."
|
|
59
|
+
message += " Problems: #{problems.join('; ')}" if problems.any?
|
|
60
|
+
redirect_to admin_holidays_path, problems.any? ? { alert: message } : { notice: message }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def holiday_params
|
|
66
|
+
params.require(:holiday).permit(:date, :name, :optional)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
# Governing tier: anything that changes policy or money — leave types,
|
|
4
|
+
# settings, office locations, holidays, employee profiles, salary
|
|
5
|
+
# structures, payroll, appraisals, audit trail. Leadership ONLY;
|
|
6
|
+
# being a host-app admin is not enough.
|
|
7
|
+
class LeadershipController < BaseController
|
|
8
|
+
skip_before_action :require_hr_admin!
|
|
9
|
+
before_action :require_hr_leadership!
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def require_hr_leadership!
|
|
14
|
+
hr_access_denied unless hr_leadership?
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
class LeaveBalancesController < BaseController
|
|
4
|
+
def index
|
|
5
|
+
year = params[:year].to_i
|
|
6
|
+
@year = year.between?(2000, 2100) ? year : Date.current.year
|
|
7
|
+
@types = LeaveType.active.where(paid: true).where.not(annual_quota: nil)
|
|
8
|
+
@employees = HrLite.employees
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Manual credit/debit — also the comp-off credit mechanism.
|
|
12
|
+
def adjust
|
|
13
|
+
user = HrLite.user_klass.find(params[:user_id])
|
|
14
|
+
type = LeaveType.find(params[:leave_type_id])
|
|
15
|
+
year = params[:year].to_i
|
|
16
|
+
delta = BigDecimal(params[:delta].to_s)
|
|
17
|
+
note = params[:note].to_s.strip
|
|
18
|
+
|
|
19
|
+
if note.blank?
|
|
20
|
+
return redirect_to admin_leave_balances_path(year: year), alert: "A note is required."
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
balance = LeaveBalance.for(user, type, year)
|
|
24
|
+
balance.adjustment += delta
|
|
25
|
+
balance.adjustment_note = [ balance.adjustment_note.presence, "#{delta.to_f} — #{note}" ].compact.join("; ")
|
|
26
|
+
balance.save!
|
|
27
|
+
|
|
28
|
+
AuditLog.create!(
|
|
29
|
+
actor: hr_current_user, action: "adjust",
|
|
30
|
+
subject_type: balance.class.name, subject_id: balance.id,
|
|
31
|
+
audited_changes: { "user" => HrLite.display_name(user), "type" => type.code,
|
|
32
|
+
"delta" => delta.to_f, "note" => note }
|
|
33
|
+
)
|
|
34
|
+
redirect_to admin_leave_balances_path(year: year),
|
|
35
|
+
notice: "Balance adjusted for #{HrLite.display_name(user)}."
|
|
36
|
+
rescue ArgumentError
|
|
37
|
+
redirect_to admin_leave_balances_path, alert: "Enter a valid adjustment number."
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
class LeaveRequestsController < BaseController
|
|
4
|
+
def index
|
|
5
|
+
scope = LeaveRequest.includes(:leave_type, :user).recent_first
|
|
6
|
+
@status = params[:status].presence_in(LeaveRequest::STATUSES) || "pending"
|
|
7
|
+
@requests = paginate(scope.where(status: @status))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
@request = LeaveRequest.includes(:leave_type).find(params[:id])
|
|
12
|
+
@balance = @request.balance
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def approve
|
|
16
|
+
request = LeaveRequest.find(params[:id])
|
|
17
|
+
if request.approve!(actor: hr_current_user, note: params[:decision_note].presence)
|
|
18
|
+
redirect_to admin_leave_requests_path, notice: "Leave approved."
|
|
19
|
+
else
|
|
20
|
+
redirect_to admin_leave_request_path(request),
|
|
21
|
+
alert: "Cannot approve — balance no longer covers this request."
|
|
22
|
+
end
|
|
23
|
+
rescue ActiveRecord::RecordInvalid
|
|
24
|
+
redirect_to admin_leave_request_path(request), alert: "Only pending requests can be decided."
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reject
|
|
28
|
+
request = LeaveRequest.find(params[:id])
|
|
29
|
+
note = params[:decision_note].to_s.strip
|
|
30
|
+
if note.blank?
|
|
31
|
+
return redirect_to admin_leave_request_path(request), alert: "A note is required to reject."
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
request.reject!(actor: hr_current_user, note: note)
|
|
35
|
+
redirect_to admin_leave_requests_path, notice: "Leave rejected."
|
|
36
|
+
rescue ActiveRecord::RecordInvalid
|
|
37
|
+
redirect_to admin_leave_request_path(request), alert: "Only pending requests can be decided."
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
class LeaveTypesController < LeadershipController
|
|
4
|
+
def index
|
|
5
|
+
@leave_types = LeaveType.order(:position, :id)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def new
|
|
9
|
+
@leave_type = LeaveType.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create
|
|
13
|
+
@leave_type = LeaveType.new(leave_type_params)
|
|
14
|
+
if @leave_type.save
|
|
15
|
+
redirect_to admin_leave_types_path, notice: "Leave type created."
|
|
16
|
+
else
|
|
17
|
+
render :new, status: :unprocessable_entity
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def edit
|
|
22
|
+
@leave_type = LeaveType.find(params[:id])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def update
|
|
26
|
+
@leave_type = LeaveType.find(params[:id])
|
|
27
|
+
if @leave_type.update(leave_type_params)
|
|
28
|
+
redirect_to admin_leave_types_path, notice: "Leave type updated."
|
|
29
|
+
else
|
|
30
|
+
render :edit, status: :unprocessable_entity
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Types with history cannot be destroyed — deactivate instead.
|
|
35
|
+
def destroy
|
|
36
|
+
leave_type = LeaveType.find(params[:id])
|
|
37
|
+
if leave_type.destroy
|
|
38
|
+
redirect_to admin_leave_types_path, notice: "Leave type removed.", status: :see_other
|
|
39
|
+
else
|
|
40
|
+
redirect_to admin_leave_types_path, status: :see_other,
|
|
41
|
+
alert: "This type has leave history — deactivate it instead."
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def leave_type_params
|
|
48
|
+
params.require(:leave_type)
|
|
49
|
+
.permit(:name, :code, :color, :paid, :annual_quota, :accrual,
|
|
50
|
+
:carry_forward_cap, :active, :position)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
class OfficeLocationsController < LeadershipController
|
|
4
|
+
def index
|
|
5
|
+
@office_locations = OfficeLocation.order(:name)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def new
|
|
9
|
+
@office_location = OfficeLocation.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create
|
|
13
|
+
@office_location = OfficeLocation.new(office_location_params)
|
|
14
|
+
if @office_location.save
|
|
15
|
+
redirect_to admin_office_locations_path, notice: "Office added."
|
|
16
|
+
else
|
|
17
|
+
render :new, status: :unprocessable_entity
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def edit
|
|
22
|
+
@office_location = OfficeLocation.find(params[:id])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def update
|
|
26
|
+
@office_location = OfficeLocation.find(params[:id])
|
|
27
|
+
if @office_location.update(office_location_params)
|
|
28
|
+
redirect_to admin_office_locations_path, notice: "Office updated."
|
|
29
|
+
else
|
|
30
|
+
render :edit, status: :unprocessable_entity
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def destroy
|
|
35
|
+
OfficeLocation.find(params[:id]).destroy!
|
|
36
|
+
redirect_to admin_office_locations_path, notice: "Office removed.", status: :see_other
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def office_location_params
|
|
42
|
+
params.require(:office_location).permit(:name, :lat, :lng, :radius_m, :active)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
class PayrollRunsController < LeadershipController
|
|
4
|
+
def index
|
|
5
|
+
@runs = paginate(PayrollRun.recent_first)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def new
|
|
9
|
+
@run = PayrollRun.new(period_month: Date.current.prev_month.beginning_of_month)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create
|
|
13
|
+
month = parse_month_param(params.dig(:payroll_run, :period_month))
|
|
14
|
+
@run = PayrollRun.new(period_month: month, created_by_id: hr_current_user.id,
|
|
15
|
+
notes: params.dig(:payroll_run, :notes))
|
|
16
|
+
if @run.save
|
|
17
|
+
redirect_to admin_payroll_run_path(@run), notice: "Run created — compute when ready."
|
|
18
|
+
else
|
|
19
|
+
render :new, status: :unprocessable_entity
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def show
|
|
24
|
+
@run = PayrollRun.find(params[:id])
|
|
25
|
+
@slips = @run.salary_slips.includes(:user).sort_by { |slip| HrLite.display_name(slip.user) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def destroy
|
|
29
|
+
run = PayrollRun.find(params[:id])
|
|
30
|
+
if run.destroy
|
|
31
|
+
redirect_to admin_payroll_runs_path, notice: "Draft run deleted.", status: :see_other
|
|
32
|
+
else
|
|
33
|
+
redirect_to admin_payroll_run_path(run), alert: run.errors.full_messages.to_sentence,
|
|
34
|
+
status: :see_other
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def compute
|
|
39
|
+
transition { |run| run.compute!(actor: hr_current_user) && "Computed — review the slips." }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def finalize
|
|
43
|
+
transition { |run| run.finalize!(actor: hr_current_user) && "Run finalized." }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def unlock
|
|
47
|
+
transition { |run| run.unlock!(actor: hr_current_user) && "Back in review." }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def publish
|
|
51
|
+
transition { |run| run.publish!(actor: hr_current_user) && "Published — employees notified." }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Payout register: the full money sheet including bank details —
|
|
55
|
+
# deliberately leadership-only PII.
|
|
56
|
+
def register
|
|
57
|
+
run = PayrollRun.find(params[:id])
|
|
58
|
+
send_data register_csv(run), filename: "payroll-register-#{run.period_month.strftime('%Y-%m')}.csv",
|
|
59
|
+
type: "text/csv"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def transition
|
|
65
|
+
run = PayrollRun.find(params[:id])
|
|
66
|
+
notice = yield(run)
|
|
67
|
+
redirect_to admin_payroll_run_path(run), notice: notice
|
|
68
|
+
rescue ActiveRecord::RecordInvalid
|
|
69
|
+
redirect_to admin_payroll_run_path(run), alert: "That step is not available from #{run.status}."
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def register_csv(run)
|
|
73
|
+
require "csv"
|
|
74
|
+
CSV.generate do |csv|
|
|
75
|
+
csv << [ "Code", "Name", "Days", "LOP", "Gross", "PF", "ESI", "PT", "TDS",
|
|
76
|
+
"Net pay", "Bank", "Account", "IFSC" ]
|
|
77
|
+
run.salary_slips.includes(:user).each do |slip|
|
|
78
|
+
profile = slip.user_profile
|
|
79
|
+
deductions = slip.deductions_rows.index_by { |row| row["code"] }
|
|
80
|
+
csv << [
|
|
81
|
+
profile&.employee_code, HrLite.display_name(slip.user),
|
|
82
|
+
slip.payable_days, slip.effective_lop_days,
|
|
83
|
+
slip.gross_earnings, deductions.dig("pf_employee", "amount") || 0,
|
|
84
|
+
deductions.dig("esi_employee", "amount") || 0, deductions.dig("pt", "amount") || 0,
|
|
85
|
+
deductions.dig("tds", "amount") || 0, slip.net_pay,
|
|
86
|
+
profile&.bank_name, profile&.bank_account_number, profile&.bank_ifsc
|
|
87
|
+
]
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|