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,44 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
class SalarySlipsController < LeadershipController
|
|
4
|
+
def show
|
|
5
|
+
@slip = SalarySlip.includes(:payroll_run).find(params[:id])
|
|
6
|
+
@profile = @slip.user_profile
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Review-stage overrides (LOP days / monthly TDS) — the slip rebuilds
|
|
10
|
+
# immediately with the overrides applied.
|
|
11
|
+
def update
|
|
12
|
+
slip = SalarySlip.includes(:payroll_run).find(params[:id])
|
|
13
|
+
run = slip.payroll_run
|
|
14
|
+
unless run.review?
|
|
15
|
+
return redirect_to admin_salary_slip_path(slip),
|
|
16
|
+
alert: "Overrides are only editable while the run is in review."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
lop_override = params.dig(:salary_slip, :lop_override).presence
|
|
20
|
+
tds_override = params.dig(:salary_slip, :tds_override).presence
|
|
21
|
+
profile = EmployeeProfile.find_by(user_id: slip.user_id)
|
|
22
|
+
structure = SalaryStructure.effective_for(slip.user, run.period_month)
|
|
23
|
+
|
|
24
|
+
attributes = SlipBuilder.call(
|
|
25
|
+
run: run, user: slip.user, structure: structure, profile: profile,
|
|
26
|
+
lop_override: lop_override && BigDecimal(lop_override),
|
|
27
|
+
tds_override: tds_override && BigDecimal(tds_override)
|
|
28
|
+
)
|
|
29
|
+
slip.update!(attributes)
|
|
30
|
+
|
|
31
|
+
AuditLog.create!(
|
|
32
|
+
actor: hr_current_user, action: "override",
|
|
33
|
+
subject_type: slip.class.name, subject_id: slip.id,
|
|
34
|
+
audited_changes: { "period" => run.label, "employee" => HrLite.display_name(slip.user),
|
|
35
|
+
"lop_override" => lop_override || "cleared",
|
|
36
|
+
"tds_override" => tds_override ? "[changed]" : "cleared" }
|
|
37
|
+
)
|
|
38
|
+
redirect_to admin_salary_slip_path(slip), notice: "Slip recomputed with overrides."
|
|
39
|
+
rescue ArgumentError
|
|
40
|
+
redirect_to admin_salary_slip_path(params[:id]), alert: "Overrides must be numbers."
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
class SalaryStructuresController < LeadershipController
|
|
4
|
+
before_action :set_profile
|
|
5
|
+
|
|
6
|
+
def new
|
|
7
|
+
@structure = SalaryStructure.new(user_id: @profile.user_id,
|
|
8
|
+
effective_from: Date.current.beginning_of_month)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def create
|
|
12
|
+
@structure = SalaryStructure.new(structure_params.merge(
|
|
13
|
+
user_id: @profile.user_id, created_by_id: hr_current_user.id
|
|
14
|
+
))
|
|
15
|
+
if @structure.save
|
|
16
|
+
redirect_to admin_employee_path(@profile), notice: "Salary structure saved."
|
|
17
|
+
else
|
|
18
|
+
render :new, status: :unprocessable_entity
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def edit
|
|
23
|
+
@structure = SalaryStructure.where(user_id: @profile.user_id).find(params[:id])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update
|
|
27
|
+
@structure = SalaryStructure.where(user_id: @profile.user_id).find(params[:id])
|
|
28
|
+
if @structure.update(structure_params)
|
|
29
|
+
redirect_to admin_employee_path(@profile), notice: "Salary structure updated."
|
|
30
|
+
else
|
|
31
|
+
render :edit, status: :unprocessable_entity
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def set_profile
|
|
38
|
+
@profile = EmployeeProfile.find(params[:employee_id])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def structure_params
|
|
42
|
+
params.require(:salary_structure).permit(
|
|
43
|
+
:effective_from, :basic, :hra, :special_allowance, :other_earnings,
|
|
44
|
+
:pf_applicable, :pf_on_full_basic, :esi_applicable, :pt_state, :notes
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
class SettingsController < LeadershipController
|
|
4
|
+
def edit
|
|
5
|
+
@setting = Setting.instance
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def update
|
|
9
|
+
@setting = Setting.instance
|
|
10
|
+
if @setting.update(params.require(:setting).permit(:weekend_policy))
|
|
11
|
+
redirect_to edit_admin_setting_path, notice: "Settings saved."
|
|
12
|
+
else
|
|
13
|
+
render :edit, status: :unprocessable_entity
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Inherits from the host-configured parent controller (set
|
|
3
|
+
# config.parent_controller in an initializer BEFORE boot; changing it needs
|
|
4
|
+
# a restart). Everything else — auth method, current user, admin/leadership
|
|
5
|
+
# checks — is resolved per request from the live config.
|
|
6
|
+
class ApplicationController < HrLite.config.parent_controller.constantize
|
|
7
|
+
layout "hr_lite/application"
|
|
8
|
+
|
|
9
|
+
before_action :hr_authenticate!
|
|
10
|
+
before_action :hr_set_current_actor
|
|
11
|
+
around_action :hr_use_time_zone
|
|
12
|
+
|
|
13
|
+
helper_method :hr_current_user, :hr_admin?, :hr_leadership?, :hr_display_name
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def hr_authenticate!
|
|
18
|
+
send(HrLite.config.authenticate_method)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def hr_current_user
|
|
22
|
+
send(HrLite.config.current_user_method)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def hr_admin?
|
|
26
|
+
HrLite.admin?(hr_current_user)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def hr_leadership?
|
|
30
|
+
HrLite.leadership?(hr_current_user)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def hr_display_name(user)
|
|
34
|
+
HrLite.display_name(user)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def hr_set_current_actor
|
|
38
|
+
HrLite::Current.actor = hr_current_user
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def hr_use_time_zone(&)
|
|
42
|
+
Time.use_zone(HrLite.config.time_zone, &)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def hr_access_denied
|
|
46
|
+
respond_to do |format|
|
|
47
|
+
format.html { redirect_to hr_lite.root_path, alert: "You do not have access to that area." }
|
|
48
|
+
format.any { head :forbidden }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Strict param parsing: anything that isn't the exact expected format
|
|
53
|
+
# falls back to today (never 500s on a mangled URL).
|
|
54
|
+
def parse_month_param(value)
|
|
55
|
+
Date.strptime(value.to_s, "%Y-%m")
|
|
56
|
+
rescue ArgumentError, TypeError
|
|
57
|
+
Date.current.beginning_of_month
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def parse_date_param(value)
|
|
61
|
+
Date.strptime(value.to_s, "%Y-%m-%d")
|
|
62
|
+
rescue ArgumentError, TypeError
|
|
63
|
+
Date.current
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Minimal LIMIT/OFFSET pagination — no host dependency.
|
|
67
|
+
def paginate(scope, per: 25)
|
|
68
|
+
@per = [ [ params.fetch(:per, per).to_i, 1 ].max, 100 ].min
|
|
69
|
+
@page = [ params.fetch(:page, 1).to_i, 1 ].max
|
|
70
|
+
@total_count = scope.count
|
|
71
|
+
@total_pages = [ (@total_count.to_f / @per).ceil, 1 ].max
|
|
72
|
+
@page = @total_pages if @page > @total_pages
|
|
73
|
+
scope.offset((@page - 1) * @per).limit(@per)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Employee view: shared appraisals only — drafts do not exist for them.
|
|
3
|
+
class AppraisalsController < ApplicationController
|
|
4
|
+
def index
|
|
5
|
+
@appraisals = paginate(own_shared.recent_first)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def show
|
|
9
|
+
@appraisal = own_shared.find(params[:id])
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def own_shared
|
|
15
|
+
Appraisal.shared.where(user_id: hr_current_user.id)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Employee self-service punch surface. Always operates on hr_current_user —
|
|
3
|
+
# there is no user param, so punching for someone else is structurally
|
|
4
|
+
# impossible.
|
|
5
|
+
class AttendanceController < ApplicationController
|
|
6
|
+
def show
|
|
7
|
+
@month = parse_month_param(params[:month])
|
|
8
|
+
@record = AttendanceRecord.find_by(user_id: hr_current_user.id, date: Date.current)
|
|
9
|
+
@day_status = DayStatus.new(user: hr_current_user, range: @month.beginning_of_month..@month.end_of_month)
|
|
10
|
+
@counts = @day_status.counts
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def check_in
|
|
14
|
+
punch(:check_in)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def check_out
|
|
18
|
+
punch(:check_out)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def punch(kind)
|
|
24
|
+
result = AttendancePuncher.call(user: hr_current_user, kind: kind, **punch_params.to_h.symbolize_keys)
|
|
25
|
+
if result.ok?
|
|
26
|
+
time = Time.current.strftime("%H:%M")
|
|
27
|
+
redirect_to attendance_path, notice: "#{kind == :check_in ? 'Checked in' : 'Checked out'} at #{time}."
|
|
28
|
+
else
|
|
29
|
+
redirect_to attendance_path, alert: result.error
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def punch_params
|
|
34
|
+
params.permit(:lat, :lng, :accuracy_m, :geo_status)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Server-rendered company calendar: holidays, everyone's approved leaves
|
|
3
|
+
# and weekend shading for a month. No JS dependency — works everywhere
|
|
4
|
+
# the engine is mounted.
|
|
5
|
+
class CalendarController < ApplicationController
|
|
6
|
+
def show
|
|
7
|
+
@month = parse_month_param(params[:month])
|
|
8
|
+
range = @month.beginning_of_month..@month.end_of_month
|
|
9
|
+
@calendar = WorkingCalendar.new(range)
|
|
10
|
+
@holidays = Holiday.in_range(range).order(:date).group_by(&:date)
|
|
11
|
+
@leaves = LeaveRequest.approved.includes(:leave_type, :user)
|
|
12
|
+
.overlapping_range(range.first, range.last)
|
|
13
|
+
.each_with_object(Hash.new { |h, k| h[k] = [] }) do |request, acc|
|
|
14
|
+
(request.start_date..request.end_date).each do |date|
|
|
15
|
+
acc[date] << request if range.cover?(date)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
class CareerController < ApplicationController
|
|
3
|
+
def show
|
|
4
|
+
@profile = EmployeeProfile.find_by(user_id: hr_current_user.id)
|
|
5
|
+
@changes = DesignationChange.where(user_id: hr_current_user.id).timeline
|
|
6
|
+
@appraisals = Appraisal.shared.where(user_id: hr_current_user.id).recent_first.limit(10)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
class HolidaysController < ApplicationController
|
|
3
|
+
def index
|
|
4
|
+
year = params[:year].to_i
|
|
5
|
+
@year = year.between?(2000, 2100) ? year : Date.current.year
|
|
6
|
+
@holidays = Holiday.where(date: Date.new(@year, 1, 1)..Date.new(@year, 12, 31)).order(:date)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
class KudosController < ApplicationController
|
|
3
|
+
def index
|
|
4
|
+
@kudos = paginate(Kudo.recent.includes(:giver, kudo_mentions: :user))
|
|
5
|
+
@kudo = Kudo.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def create
|
|
9
|
+
@kudo = Kudo.new(kudo_params.merge(giver: hr_current_user))
|
|
10
|
+
if @kudo.save
|
|
11
|
+
@kudo.register_mentions!
|
|
12
|
+
redirect_to kudos_path, notice: "Kudos posted."
|
|
13
|
+
else
|
|
14
|
+
@kudos = paginate(Kudo.recent.includes(:giver, kudo_mentions: :user))
|
|
15
|
+
render :index, status: :unprocessable_entity
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def destroy
|
|
20
|
+
kudo = Kudo.find(params[:id])
|
|
21
|
+
if kudo.deletable_by?(hr_current_user)
|
|
22
|
+
kudo.destroy!
|
|
23
|
+
redirect_to kudos_path, notice: "Kudos removed.", status: :see_other
|
|
24
|
+
else
|
|
25
|
+
redirect_to kudos_path, alert: "You can only remove your own kudos within 15 minutes.",
|
|
26
|
+
status: :see_other
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def kudo_params
|
|
33
|
+
params.require(:kudo).permit(:message, :badge)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
class LeaveBalancesController < ApplicationController
|
|
3
|
+
def index
|
|
4
|
+
year = params[:year].to_i
|
|
5
|
+
@year = year.between?(2000, 2100) ? year : Date.current.year
|
|
6
|
+
@balances = LeaveType.active.where(paid: true).where.not(annual_quota: nil).map do |type|
|
|
7
|
+
LeaveBalance.for(hr_current_user, type, @year)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Employee self-service: always scoped to hr_current_user — a foreign id
|
|
3
|
+
# 404s, never 403s.
|
|
4
|
+
class LeaveRequestsController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@requests = paginate(own_requests.recent_first.includes(:leave_type))
|
|
7
|
+
@balances = balance_cards
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
@request = own_requests.find(params[:id])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def new
|
|
15
|
+
@request = LeaveRequest.new(start_date: Date.current, end_date: Date.current)
|
|
16
|
+
@balances = balance_cards
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create
|
|
20
|
+
@request = LeaveRequest.new(request_params.merge(user_id: hr_current_user.id))
|
|
21
|
+
if @request.save
|
|
22
|
+
redirect_to leave_requests_path, notice: "Leave request submitted."
|
|
23
|
+
else
|
|
24
|
+
@balances = balance_cards
|
|
25
|
+
render :new, status: :unprocessable_entity
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def cancel
|
|
30
|
+
request = own_requests.find(params[:id])
|
|
31
|
+
if request.cancellable_by?(hr_current_user)
|
|
32
|
+
request.cancel!(actor: hr_current_user)
|
|
33
|
+
redirect_to leave_requests_path, notice: "Leave cancelled."
|
|
34
|
+
else
|
|
35
|
+
redirect_to leave_requests_path, alert: "This request can no longer be cancelled."
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def own_requests
|
|
42
|
+
LeaveRequest.where(user_id: hr_current_user.id)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def balance_cards
|
|
46
|
+
year = Date.current.year
|
|
47
|
+
LeaveType.active.where(paid: true).where.not(annual_quota: nil).map do |type|
|
|
48
|
+
LeaveBalance.for(hr_current_user, type, year)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def request_params
|
|
53
|
+
params.require(:leave_request).permit(:leave_type_id, :start_date, :end_date, :half_day, :reason)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Employee surface: strictly own PUBLISHED slips — a foreign or
|
|
3
|
+
# unpublished id 404s through the scoped relation, never 403s.
|
|
4
|
+
class SalarySlipsController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@slips = paginate(own_published.recent_first)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def show
|
|
10
|
+
@slip = own_published.find(params[:id])
|
|
11
|
+
|
|
12
|
+
respond_to do |format|
|
|
13
|
+
format.html
|
|
14
|
+
format.pdf { send_slip_pdf(@slip) }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def own_published
|
|
21
|
+
SalarySlip.published.where(user_id: hr_current_user.id)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def send_slip_pdf(slip)
|
|
25
|
+
pdf = PdfRenderer.render(
|
|
26
|
+
template: "hr_lite/salary_slips/pdf",
|
|
27
|
+
assigns: { slip: slip, profile: slip.user_profile },
|
|
28
|
+
cache_key: slip.pdf_cache_key
|
|
29
|
+
)
|
|
30
|
+
if pdf
|
|
31
|
+
send_data pdf, filename: "salary-slip-#{slip.period_month.strftime('%Y-%m')}.pdf",
|
|
32
|
+
type: "application/pdf", disposition: "inline"
|
|
33
|
+
else
|
|
34
|
+
redirect_to salary_slip_path(slip),
|
|
35
|
+
alert: "PDF downloads are not configured on this install."
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Mention autocomplete source. Available to any signed-in staff member.
|
|
3
|
+
class UsersController < ApplicationController
|
|
4
|
+
def search
|
|
5
|
+
users = HrLite.config.mentionable_users.call(params[:q].to_s)
|
|
6
|
+
render json: users.map { |u| { value: u.id, text: HrLite.display_name(u) } }
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module ApplicationHelper
|
|
3
|
+
NAV_ITEMS = [
|
|
4
|
+
{ label: "Home", path: :root_path, match: [ "/" ] },
|
|
5
|
+
{ label: "Attendance", path: :attendance_path, match: [ "/attendance" ] },
|
|
6
|
+
{ label: "Leaves", path: :leave_requests_path, match: [ "/leave_requests", "/leave_balances" ] },
|
|
7
|
+
{ label: "Calendar", path: :calendar_path, match: [ "/calendar", "/holidays" ] },
|
|
8
|
+
{ label: "Kudos", path: :kudos_path, match: [ "/kudos" ] },
|
|
9
|
+
{ label: "Slips", path: :salary_slips_path, match: [ "/salary_slips" ] },
|
|
10
|
+
{ label: "Career", path: :career_path, match: [ "/career", "/appraisals", "/profile" ] }
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
ADMIN_NAV_ITEMS = [
|
|
14
|
+
{ label: "Overview", path: :admin_overview_path, match: [ "/admin/overview" ] },
|
|
15
|
+
{ label: "Team attendance", path: :admin_attendances_path, match: [ "/admin/attendances" ] },
|
|
16
|
+
{ label: "Approvals", path: :admin_leave_requests_path, match: [ "/admin/leave_requests", "/admin/leave_balances" ] }
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
LEADERSHIP_NAV_ITEMS = [
|
|
20
|
+
{ label: "Employees", path: :admin_employees_path, match: [ "/admin/employees" ] },
|
|
21
|
+
{ label: "Payroll", path: :admin_payroll_runs_path, match: [ "/admin/payroll_runs", "/admin/salary_slips" ] },
|
|
22
|
+
{ label: "Settings", path: :admin_leave_types_path, match: [ "/admin/leave_types", "/admin/office_locations", "/admin/holidays", "/admin/setting" ] },
|
|
23
|
+
{ label: "Audit", path: :admin_audit_logs_path, match: [ "/admin/audit_logs" ] }
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
# Only items whose routes exist yet (the nav grows with each phase).
|
|
27
|
+
def hrl_nav_items(items)
|
|
28
|
+
items.select { |item| hr_lite_route?(item[:path]) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def hr_lite_route?(helper_name)
|
|
32
|
+
hr_lite.respond_to?(helper_name)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def hrl_nav_link(item)
|
|
36
|
+
href = hr_lite.public_send(item[:path])
|
|
37
|
+
active = item[:match].any? do |prefix|
|
|
38
|
+
prefix == "/" ? request.path == href : request.path.start_with?("#{hr_lite.root_path.chomp('/')}#{prefix}")
|
|
39
|
+
end
|
|
40
|
+
link_to item[:label], href, class: "hrl-nav__link #{'hrl-nav__link--active' if active}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Mention-marker-aware, XSS-safe kudos message rendering. Every literal
|
|
44
|
+
# chunk is escaped; only markers whose id has a mention row become
|
|
45
|
+
# highlighted chips (with the CURRENT display name — stale-name safe).
|
|
46
|
+
def render_kudo_message(kudo)
|
|
47
|
+
mentioned = kudo.kudo_mentions.index_by(&:user_id)
|
|
48
|
+
out = +""
|
|
49
|
+
rest = kudo.message.to_s
|
|
50
|
+
while (m = HrLite::MentionParser::MARKER.match(rest))
|
|
51
|
+
out << ERB::Util.html_escape(m.pre_match)
|
|
52
|
+
mention = mentioned[m[2].to_i]
|
|
53
|
+
if mention
|
|
54
|
+
out << %(<span class="hrl-mention">@#{ERB::Util.html_escape(HrLite.display_name(mention.user))}</span>)
|
|
55
|
+
else
|
|
56
|
+
out << ERB::Util.html_escape("@#{m[1]}")
|
|
57
|
+
end
|
|
58
|
+
rest = m.post_match
|
|
59
|
+
end
|
|
60
|
+
out << ERB::Util.html_escape(rest)
|
|
61
|
+
out.html_safe
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def hrl_pagination
|
|
65
|
+
render "hr_lite/shared/pagination"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def hrl_money(amount)
|
|
69
|
+
return "—" if amount.nil?
|
|
70
|
+
|
|
71
|
+
whole, fraction = HrLite::Money.round2(amount).to_s("F").split(".")
|
|
72
|
+
sign = whole.delete_prefix!("-") ? "-" : ""
|
|
73
|
+
# Indian digit grouping: 12,34,567
|
|
74
|
+
grouped = whole.reverse.scan(/\d{1,2}(?=\d{2})|\d{1,3}/).join(",").reverse if whole.length > 3
|
|
75
|
+
grouped ||= whole
|
|
76
|
+
"#{sign}#{HrLite.config.currency_symbol}#{grouped}.#{fraction.ljust(2, '0')}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
ONES = %w[zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen
|
|
80
|
+
fifteen sixteen seventeen eighteen nineteen].freeze
|
|
81
|
+
TENS = %w[zero ten twenty thirty forty fifty sixty seventy eighty ninety].freeze
|
|
82
|
+
|
|
83
|
+
# Indian-system amount in words for the slip footer.
|
|
84
|
+
def hrl_amount_in_words(amount)
|
|
85
|
+
rupees = HrLite::Money.round_rupee(amount).to_i
|
|
86
|
+
return "Zero rupees" if rupees.zero?
|
|
87
|
+
|
|
88
|
+
parts = []
|
|
89
|
+
[ [ 10_000_000, "crore" ], [ 100_000, "lakh" ], [ 1000, "thousand" ], [ 100, "hundred" ] ].each do |divisor, label|
|
|
90
|
+
next unless rupees >= divisor
|
|
91
|
+
|
|
92
|
+
parts << "#{hrl_two_digit_words(rupees / divisor)} #{label}"
|
|
93
|
+
rupees %= divisor
|
|
94
|
+
end
|
|
95
|
+
parts << hrl_two_digit_words(rupees) if rupees.positive?
|
|
96
|
+
"#{parts.join(' ').capitalize} rupees"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def hrl_two_digit_words(number)
|
|
100
|
+
return ONES[number] if number < 20
|
|
101
|
+
|
|
102
|
+
[ TENS[number / 10], number % 10 == 0 ? nil : ONES[number % 10] ].compact.join("-")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Morning leadership email: who's out, what's pending, what's flagged.
|
|
3
|
+
# Sends nothing on a quiet day.
|
|
4
|
+
class DailyDigestJob < ActiveJob::Base
|
|
5
|
+
queue_as :default
|
|
6
|
+
|
|
7
|
+
def perform(date: Date.current)
|
|
8
|
+
query = OverviewQuery.new(date: date)
|
|
9
|
+
return if query.empty?
|
|
10
|
+
|
|
11
|
+
lines = []
|
|
12
|
+
query.on_leave_today.each do |request|
|
|
13
|
+
lines << "On leave: #{HrLite.display_name(request.user)} — #{request.leave_type.name} (#{request.date_range_label})"
|
|
14
|
+
end
|
|
15
|
+
query.pending_requests.each do |request|
|
|
16
|
+
lines << "Pending approval: #{HrLite.display_name(request.user)} — #{request.leave_type.name} (#{request.date_range_label})"
|
|
17
|
+
end
|
|
18
|
+
query.flagged_today.each do |record|
|
|
19
|
+
lines << "Flagged punch: #{HrLite.display_name(record.user)} — #{record.flag_note}"
|
|
20
|
+
end
|
|
21
|
+
query.missing_checkout_yesterday.each do |record|
|
|
22
|
+
lines << "Missing checkout yesterday: #{HrLite.display_name(record.user)}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Notifications.publish(
|
|
26
|
+
"digest.daily",
|
|
27
|
+
title: "HR daily digest — #{date.strftime('%d %b %Y')}",
|
|
28
|
+
lines: lines,
|
|
29
|
+
path: "/admin/overview"
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Jan-1 rollover: materialize carry-forward into the new year's balance
|
|
3
|
+
# rows. Idempotent — a balance whose carry has already been written is
|
|
4
|
+
# never touched again (manual adjustments stay safe).
|
|
5
|
+
class LeaveYearRolloverJob < ActiveJob::Base
|
|
6
|
+
queue_as :default
|
|
7
|
+
|
|
8
|
+
def perform(year: Date.current.year)
|
|
9
|
+
previous_year = year - 1
|
|
10
|
+
|
|
11
|
+
LeaveType.active.where(paid: true).where.not(annual_quota: nil).find_each do |type|
|
|
12
|
+
next unless type.carry_forward_cap.positive?
|
|
13
|
+
|
|
14
|
+
HrLite.employees.each do |user|
|
|
15
|
+
balance = LeaveBalance.for(user, type, year)
|
|
16
|
+
next if balance.persisted? && balance.carried_forward.positive?
|
|
17
|
+
|
|
18
|
+
carry = LeaveBalance.for(user, type, previous_year)
|
|
19
|
+
.available(as_of: Date.new(previous_year, 12, 31))
|
|
20
|
+
carry = [ [ carry, type.carry_forward_cap ].min, 0 ].max
|
|
21
|
+
next if carry.zero? && balance.persisted?
|
|
22
|
+
|
|
23
|
+
balance.carried_forward = carry
|
|
24
|
+
balance.save!
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|