hr_lite 0.54.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +60 -0
- data/app/assets/javascripts/hr_lite/confirm.js +9 -5
- data/app/assets/javascripts/hr_lite/countdown.js +9 -2
- data/app/assets/javascripts/hr_lite/search.js +1 -1
- data/app/assets/stylesheets/hr_lite/hr_lite.css +18 -9
- data/app/controllers/hr_lite/admin/attendances_controller.rb +10 -3
- data/app/controllers/hr_lite/admin/audit_logs_controller.rb +1 -0
- data/app/controllers/hr_lite/admin/employees_controller.rb +11 -4
- data/app/controllers/hr_lite/admin/expenses_controller.rb +4 -0
- data/app/controllers/hr_lite/admin/hr_requests_controller.rb +4 -0
- data/app/controllers/hr_lite/admin/leave_requests_controller.rb +4 -1
- data/app/controllers/hr_lite/admin/payroll_runs_controller.rb +9 -5
- data/app/controllers/hr_lite/admin/reports_controller.rb +3 -2
- data/app/controllers/hr_lite/admin/salary_structures_controller.rb +2 -0
- data/app/controllers/hr_lite/admin/tax_declarations_controller.rb +13 -3
- data/app/controllers/hr_lite/approvals_controller.rb +1 -0
- data/app/controllers/hr_lite/home_controller.rb +2 -2
- data/app/controllers/hr_lite/regularization_requests_controller.rb +4 -4
- data/app/controllers/hr_lite/tax_declarations_controller.rb +9 -5
- data/app/helpers/hr_lite/application_helper.rb +14 -8
- data/app/jobs/hr_lite/attendance_close_job.rb +1 -1
- data/app/jobs/hr_lite/payroll_compute_job.rb +12 -0
- data/app/models/concerns/hr_lite/audited.rb +3 -1
- data/app/models/concerns/hr_lite/encrypted_money.rb +8 -5
- data/app/models/hr_lite/attendance_record.rb +14 -0
- data/app/models/hr_lite/comp_off_request.rb +2 -0
- data/app/models/hr_lite/document.rb +1 -1
- data/app/models/hr_lite/expense.rb +3 -0
- data/app/models/hr_lite/hr_request.rb +2 -0
- data/app/models/hr_lite/kudo.rb +2 -2
- data/app/models/hr_lite/leave_request.rb +25 -6
- data/app/models/hr_lite/payroll_line_item.rb +8 -0
- data/app/models/hr_lite/payroll_run.rb +15 -6
- data/app/models/hr_lite/regularization_request.rb +2 -2
- data/app/models/hr_lite/resignation.rb +5 -1
- data/app/models/hr_lite/role.rb +5 -0
- data/app/models/hr_lite/salary_slip.rb +1 -1
- data/app/models/hr_lite/tax_declaration.rb +5 -0
- data/app/services/hr_lite/attendance_puncher.rb +4 -0
- data/app/services/hr_lite/overview_query.rb +0 -12
- data/app/services/hr_lite/work_status.rb +5 -2
- data/app/views/hr_lite/admin/assets/index.html.erb +9 -2
- data/app/views/hr_lite/admin/attendances/show.html.erb +3 -1
- data/app/views/hr_lite/admin/attendances/summary.html.erb +5 -2
- data/app/views/hr_lite/admin/audit_logs/index.html.erb +6 -4
- data/app/views/hr_lite/admin/ca_reviews/index.html.erb +3 -3
- data/app/views/hr_lite/admin/checklists/index.html.erb +3 -3
- data/app/views/hr_lite/admin/comp_off_requests/index.html.erb +1 -2
- data/app/views/hr_lite/admin/comp_off_requests/show.html.erb +5 -5
- data/app/views/hr_lite/admin/designation_changes/new.html.erb +4 -3
- data/app/views/hr_lite/admin/documents/index.html.erb +8 -3
- data/app/views/hr_lite/admin/documents/show.html.erb +1 -0
- data/app/views/hr_lite/admin/employees/_form.html.erb +12 -5
- data/app/views/hr_lite/admin/employees/show.html.erb +38 -32
- data/app/views/hr_lite/admin/expenses/index.html.erb +3 -2
- data/app/views/hr_lite/admin/holidays/index.html.erb +5 -4
- data/app/views/hr_lite/admin/hr_requests/index.html.erb +1 -1
- data/app/views/hr_lite/admin/hr_requests/show.html.erb +4 -3
- data/app/views/hr_lite/admin/leave_balances/index.html.erb +6 -4
- data/app/views/hr_lite/admin/leave_requests/index.html.erb +1 -2
- data/app/views/hr_lite/admin/leave_requests/show.html.erb +5 -3
- data/app/views/hr_lite/admin/leave_types/edit.html.erb +4 -4
- data/app/views/hr_lite/admin/leave_types/index.html.erb +3 -3
- data/app/views/hr_lite/admin/loans/index.html.erb +1 -1
- data/app/views/hr_lite/admin/loans/show.html.erb +8 -12
- data/app/views/hr_lite/admin/office_locations/edit.html.erb +4 -4
- data/app/views/hr_lite/admin/payroll_runs/index.html.erb +2 -2
- data/app/views/hr_lite/admin/payroll_runs/show.html.erb +10 -7
- data/app/views/hr_lite/admin/policies/index.html.erb +1 -1
- data/app/views/hr_lite/admin/policies/new.html.erb +2 -2
- data/app/views/hr_lite/admin/regularization_requests/index.html.erb +3 -4
- data/app/views/hr_lite/admin/regularization_requests/show.html.erb +4 -5
- data/app/views/hr_lite/admin/reports/index.html.erb +3 -6
- data/app/views/hr_lite/admin/reports/show.html.erb +16 -2
- data/app/views/hr_lite/admin/roles/_form.html.erb +6 -5
- data/app/views/hr_lite/admin/salary_slips/show.html.erb +3 -2
- data/app/views/hr_lite/admin/salary_structures/_form.html.erb +1 -0
- data/app/views/hr_lite/admin/settings/edit.html.erb +5 -1
- data/app/views/hr_lite/admin/statutory_rate_cards/_form.html.erb +3 -3
- data/app/views/hr_lite/admin/statutory_rate_cards/index.html.erb +3 -1
- data/app/views/hr_lite/admin/tax_declarations/index.html.erb +1 -1
- data/app/views/hr_lite/admin/tax_declarations/show.html.erb +2 -0
- data/app/views/hr_lite/admin/wfh_requests/index.html.erb +17 -16
- data/app/views/hr_lite/admin/wfh_requests/show.html.erb +2 -4
- data/app/views/hr_lite/admin/work_locations/index.html.erb +1 -1
- data/app/views/hr_lite/admin/work_modes/index.html.erb +1 -1
- data/app/views/hr_lite/approvals/index.html.erb +1 -1
- data/app/views/hr_lite/attendance/_month_grid.html.erb +9 -9
- data/app/views/hr_lite/attendance/_penalties.html.erb +2 -2
- data/app/views/hr_lite/attendance/_punch_card.html.erb +2 -4
- data/app/views/hr_lite/attendance/_slot.html.erb +9 -6
- data/app/views/hr_lite/attendance/_trail.html.erb +1 -1
- data/app/views/hr_lite/attendance/_work_mode.html.erb +1 -1
- data/app/views/hr_lite/attendance/show.html.erb +4 -4
- data/app/views/hr_lite/calendar/show.html.erb +2 -2
- data/app/views/hr_lite/comp_off_requests/index.html.erb +1 -1
- data/app/views/hr_lite/comp_off_requests/new.html.erb +1 -1
- data/app/views/hr_lite/documents/_upload_form.html.erb +1 -1
- data/app/views/hr_lite/employee_profiles/show.html.erb +4 -2
- data/app/views/hr_lite/home/index.html.erb +2 -2
- data/app/views/hr_lite/id_cards/show.html.erb +5 -1
- data/app/views/hr_lite/leave_requests/_balance_chips.html.erb +2 -2
- data/app/views/hr_lite/leave_requests/new.html.erb +17 -8
- data/app/views/hr_lite/leave_requests/show.html.erb +2 -0
- data/app/views/hr_lite/org/show.html.erb +1 -1
- data/app/views/hr_lite/regularization_requests/index.html.erb +7 -7
- data/app/views/hr_lite/regularization_requests/new.html.erb +7 -5
- data/app/views/hr_lite/salary_slips/_slip_detail.html.erb +2 -2
- data/app/views/hr_lite/salary_slips/pdf.html.erb +1 -1
- data/app/views/hr_lite/tax/_computation.html.erb +1 -1
- data/app/views/hr_lite/tax_declarations/show.html.erb +4 -4
- data/app/views/hr_lite/team/show.html.erb +8 -7
- data/app/views/hr_lite/timelines/show.html.erb +1 -0
- data/app/views/hr_lite/wfh_requests/index.html.erb +3 -3
- data/app/views/hr_lite/work_time/_bars.html.erb +1 -1
- data/app/views/hr_lite/work_time/_donut.html.erb +1 -1
- data/app/views/hr_lite/work_time/_report.html.erb +14 -8
- data/app/views/layouts/hr_lite/application.html.erb +1 -0
- data/lib/hr_lite/notifications.rb +5 -2
- data/lib/hr_lite/version.rb +1 -1
- metadata +2 -1
|
@@ -41,7 +41,7 @@ module HrLite
|
|
|
41
41
|
queue: -> { hrl_queue(HrLite::LeaveRequest.where(status: "pending"), "leave.approve") } },
|
|
42
42
|
{ label: "Comp-off", path: :admin_comp_off_requests_path, match: [ "/admin/comp_off_requests" ], if: OPS,
|
|
43
43
|
queue: -> { hrl_queue(HrLite::CompOffRequest.pending, "leave.approve") } },
|
|
44
|
-
{ label: "
|
|
44
|
+
{ label: "Day fixes", path: :admin_regularization_requests_path, match: [ "/admin/regularization_requests" ], if: OPS,
|
|
45
45
|
queue: -> { hrl_queue(HrLite::RegularizationRequest.pending, "attendance.manage") } },
|
|
46
46
|
{ label: "WFH", path: :admin_wfh_requests_path, match: [ "/admin/wfh_requests" ], if: OPS,
|
|
47
47
|
queue: -> { hrl_queue(HrLite::WfhRequest.pending.overlapping(Date.current, Date.current + HrLite::WfhRequest::MAX_DAYS_AHEAD), "attendance.manage") } },
|
|
@@ -49,6 +49,8 @@ module HrLite
|
|
|
49
49
|
queue: -> { hrl_queue(HrLite::WorkLocation.pending, "attendance.manage") } },
|
|
50
50
|
{ label: "Claims", path: :admin_expenses_path, match: [ "/admin/expenses" ], if: -> { hr_can?("expense.approve", scope: :team) },
|
|
51
51
|
queue: -> { hrl_queue(HrLite::Expense.where(status: "submitted"), "expense.approve") } },
|
|
52
|
+
{ label: "Resignations", path: :admin_employees_path, match: [ "/admin/resignations" ], if: LEAD,
|
|
53
|
+
queue: -> { hrl_queue(HrLite::Resignation.pending, "profile.manage") } },
|
|
52
54
|
{ label: "Documents to verify", path: :admin_documents_path, match: [ "/admin/documents" ], if: LEAD,
|
|
53
55
|
queue: -> { hrl_queue(HrLite::Document.where(verification: "pending"), "document.view") } },
|
|
54
56
|
{ label: "CA review", path: :admin_ca_reviews_path, match: [ "/admin/ca_reviews" ],
|
|
@@ -69,11 +71,11 @@ module HrLite
|
|
|
69
71
|
{ label: "Team benefits", path: :admin_benefits_path, match: [ "/admin/benefits" ], if: MANAGE }
|
|
70
72
|
] },
|
|
71
73
|
{ label: "People", match: [ "/people" ], tabs: [
|
|
72
|
-
{ label: "
|
|
74
|
+
{ label: "Team board", path: :team_path, match: [ "/team" ] },
|
|
73
75
|
{ label: "Org chart", path: :org_path, match: [ "/org" ] },
|
|
74
76
|
{ label: "Kudos", path: :kudos_path, match: [ "/kudos" ] },
|
|
75
77
|
{ label: "Employees", path: :admin_employees_path, match: [ "/admin/employees" ], if: LEAD },
|
|
76
|
-
{ label: "Joining & exits", path: :admin_checklists_path, match: [ "/admin/checklists" ], if:
|
|
78
|
+
{ label: "Joining & exits", path: :admin_checklists_path, match: [ "/admin/checklists" ], if: -> { hr_can?("checklist.manage", scope: :all) } },
|
|
77
79
|
{ label: "Awards", path: :admin_awards_path, match: [ "/admin/awards" ], if: LEAD },
|
|
78
80
|
{ label: "PIPs", path: :admin_performance_plans_path, match: [ "/admin/performance_plans" ], if: SUPER }
|
|
79
81
|
] },
|
|
@@ -87,7 +89,7 @@ module HrLite
|
|
|
87
89
|
{ label: "Help", tabs: [
|
|
88
90
|
{ label: "Policies", path: :policies_path, match: [ "/policies" ], if: -> { hr_can?("policy.view", scope: :all) } },
|
|
89
91
|
{ label: "Ask HR", path: :hr_requests_path, match: [ "/hr_requests" ], if: -> { hr_can?("hr_request.raise") } },
|
|
90
|
-
{ label: "Help desk", path: :admin_hr_requests_path, match: [ "/admin/hr_requests" ], if:
|
|
92
|
+
{ label: "Help desk", path: :admin_hr_requests_path, match: [ "/admin/hr_requests" ], if: -> { hr_can?("hr_request.manage", scope: :all) } },
|
|
91
93
|
{ label: "Manage policies", path: :admin_policies_path, match: [ "/admin/policies" ], if: -> { hr_can?("policy.manage", scope: :all) } }
|
|
92
94
|
] },
|
|
93
95
|
{ label: "Admin", divider: true, tabs: [
|
|
@@ -96,7 +98,7 @@ module HrLite
|
|
|
96
98
|
{ label: "Roles", path: :admin_roles_path, match: [ "/admin/roles" ], if: -> { hr_can?("role.manage", scope: :all) } },
|
|
97
99
|
{ label: "Statutory rates", path: :admin_statutory_rate_cards_path, match: [ "/admin/statutory_rate_cards" ], if: SUPER },
|
|
98
100
|
{ label: "Reports", path: :admin_reports_path, match: [ "/admin/reports" ], if: -> { hr_can?("expense.approve", scope: :all) || %w[leave.view attendance.view profile.view].any? { |key| hr_can?(key, scope: :team) } } },
|
|
99
|
-
{ label: "Assets", path: :admin_assets_path, match: [ "/admin/assets" ], if:
|
|
101
|
+
{ label: "Assets", path: :admin_assets_path, match: [ "/admin/assets" ], if: -> { hr_can?("asset.manage", scope: :all) } },
|
|
100
102
|
{ label: "Audit trail", path: :admin_audit_logs_path, match: [ "/admin/audit_logs" ], if: LEAD }
|
|
101
103
|
] }
|
|
102
104
|
].freeze
|
|
@@ -164,8 +166,12 @@ module HrLite
|
|
|
164
166
|
when HrLite::LeaveRequest
|
|
165
167
|
[ "#{record.leave_type.name}, #{record.date_range_label} · #{pluralize(hrl_days(record.days_count), 'day')}", hr_lite.admin_leave_request_path(record) ]
|
|
166
168
|
when HrLite::CompOffRequest then [ "Comp-off for #{record.date_worked.strftime('%a %-d %b')}", hr_lite.admin_comp_off_request_path(record) ]
|
|
167
|
-
when HrLite::RegularizationRequest then [ "
|
|
169
|
+
when HrLite::RegularizationRequest then [ "Day fix, #{record.date.strftime('%a %-d %b')}", hr_lite.admin_regularization_request_path(record) ]
|
|
168
170
|
when HrLite::WfhRequest then [ "WFH, #{record.dates_label}", hr_lite.admin_wfh_request_path(record) ]
|
|
171
|
+
when HrLite::Expense then [ "Claim: #{record.category.name}, #{hrl_money(record.amount)}, #{record.spent_on.strftime('%a %-d %b')}", hr_lite.admin_expenses_path ]
|
|
172
|
+
when HrLite::Resignation
|
|
173
|
+
profile = HrLite::EmployeeProfile.find_by(user_id: record.user_id)
|
|
174
|
+
[ "Resignation, last day #{record.proposed_last_day.strftime('%-d %b %Y')}", profile ? hr_lite.admin_employee_path(profile) : hr_lite.admin_employees_path ]
|
|
169
175
|
else [ tab[:label], hr_lite.public_send(tab[:path]) ]
|
|
170
176
|
end
|
|
171
177
|
end
|
|
@@ -300,8 +306,8 @@ module HrLite
|
|
|
300
306
|
# Status pill for a request lifecycle string — one colour map for every
|
|
301
307
|
# list and show screen.
|
|
302
308
|
def hrl_request_status_badge(status)
|
|
303
|
-
css = { "approved" => "hrl-badge--ok", "
|
|
304
|
-
"cancelled" => "hrl-badge--muted" }[status]
|
|
309
|
+
css = { "approved" => "hrl-badge--ok", "reimbursed" => "hrl-badge--ok", "verified" => "hrl-badge--ok",
|
|
310
|
+
"rejected" => "hrl-badge--bad", "cancelled" => "hrl-badge--muted", "draft" => "hrl-badge--muted" }[status]
|
|
305
311
|
content_tag(:span, status.humanize, class: "hrl-badge #{css}")
|
|
306
312
|
end
|
|
307
313
|
|
|
@@ -58,7 +58,7 @@ module HrLite
|
|
|
58
58
|
"Fix it yourself by #{(date + rule[:within_days]).strftime('%d %b')} (up to #{rule[:per_week]} a week), " \
|
|
59
59
|
"or raise a ticket for HR."
|
|
60
60
|
else
|
|
61
|
-
"
|
|
61
|
+
"Use Fix a day to get it fixed."
|
|
62
62
|
end
|
|
63
63
|
Notifications.publish(event, title: title, body: body, path: "/regularization_requests/new?date=#{date.iso8601}",
|
|
64
64
|
bell_to: [ user ], email_to: [ user ])
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Compute off the request (PayrollRun#compute_later!). A failure restores
|
|
3
|
+
# the run's status and lands as a run warning instead of a 500.
|
|
4
|
+
class PayrollComputeJob < ApplicationJob
|
|
5
|
+
def perform(run, from, actor)
|
|
6
|
+
run.compute!(actor: actor, from: from)
|
|
7
|
+
rescue => e
|
|
8
|
+
Rails.error.report(e)
|
|
9
|
+
run.reload.update_columns(warnings: run.warnings | [ "Compute failed (#{e.message.truncate(150)}) — nothing changed; try again." ]) # rubocop:disable Rails/SkipsModelValidations
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -8,6 +8,8 @@ module HrLite
|
|
|
8
8
|
|
|
9
9
|
SKIPPED_ATTRIBUTES = %w[updated_at created_at].freeze
|
|
10
10
|
REDACTED = "[changed]".freeze
|
|
11
|
+
# Personal text stored in the clear: the trail says it changed, never to what.
|
|
12
|
+
PII_COLUMNS = { "HrLite::EmployeeProfile" => %w[date_of_birth], "HrLite::HrRequest" => %w[subject body] }.freeze
|
|
11
13
|
|
|
12
14
|
# *_commit, not *_create/_update/_destroy: these callbacks write an audit
|
|
13
15
|
# row and email leadership a diff. Fired inside the transaction they
|
|
@@ -40,7 +42,7 @@ module HrLite
|
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
def hr_lite_audited_changes(action)
|
|
43
|
-
encrypted = self.class.try(:encrypted_attributes)&.map(&:to_s) || []
|
|
45
|
+
encrypted = (self.class.try(:encrypted_attributes)&.map(&:to_s) || []) + PII_COLUMNS.fetch(self.class.name, [])
|
|
44
46
|
|
|
45
47
|
case action
|
|
46
48
|
when "destroy"
|
|
@@ -8,6 +8,10 @@ module HrLite
|
|
|
8
8
|
module EncryptedMoney
|
|
9
9
|
extend ActiveSupport::Concern
|
|
10
10
|
|
|
11
|
+
included do
|
|
12
|
+
validate { @unreadable_money&.each { |attribute| errors.add(attribute, "is not a number") } }
|
|
13
|
+
end
|
|
14
|
+
|
|
11
15
|
class_methods do
|
|
12
16
|
def encrypted_money(*attributes)
|
|
13
17
|
attributes.each do |attribute|
|
|
@@ -19,11 +23,10 @@ module HrLite
|
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
define_method("#{attribute}=") do |value|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
26
|
+
digits = value.to_s.gsub(/[,₹\s]/, "") # "1,50,000" and "₹ 1200" are how people type money
|
|
27
|
+
super(digits.empty? ? nil : BigDecimal(digits).round(2).to_s("F"))
|
|
28
|
+
rescue ArgumentError
|
|
29
|
+
(@unreadable_money ||= []) << attribute
|
|
27
30
|
end
|
|
28
31
|
end
|
|
29
32
|
end
|
|
@@ -35,6 +35,7 @@ module HrLite
|
|
|
35
35
|
validates :status, inclusion: { in: STATUSES }
|
|
36
36
|
validates :half_day_part, inclusion: { in: %w[first second] }, allow_blank: true
|
|
37
37
|
validate :check_out_after_check_in
|
|
38
|
+
validate :times_on_the_day, if: -> { will_save_change_to_date? || will_save_change_to_check_in_at? || will_save_change_to_check_out_at? }
|
|
38
39
|
|
|
39
40
|
scope :for_date, ->(date) { where(date: date) }
|
|
40
41
|
scope :for_month, ->(month) { where(date: month.beginning_of_month..month.end_of_month) }
|
|
@@ -118,5 +119,18 @@ module HrLite
|
|
|
118
119
|
|
|
119
120
|
errors.add(:check_out_at, "must be after check-in")
|
|
120
121
|
end
|
|
122
|
+
|
|
123
|
+
# No future days; check-out may run into the next morning (night shift).
|
|
124
|
+
# Only the fields being changed, in the company's zone, so re-saving an old row never fails.
|
|
125
|
+
def times_on_the_day
|
|
126
|
+
return unless date
|
|
127
|
+
|
|
128
|
+
day = ->(time) { time.in_time_zone(HrLite.config.time_zone).to_date }
|
|
129
|
+
errors.add(:date, "can't be in the future") if will_save_change_to_date? && date > day.(Time.current)
|
|
130
|
+
errors.add(:check_in_at, "must be on #{date.strftime('%d %b')}") if will_save_change_to_check_in_at? && check_in_at && day.(check_in_at) != date
|
|
131
|
+
if will_save_change_to_check_out_at? && check_out_at && !(date..date + 1).cover?(day.(check_out_at))
|
|
132
|
+
errors.add(:check_out_at, "must be on #{date.strftime('%d %b')} or the next morning")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
121
135
|
end
|
|
122
136
|
end
|
|
@@ -50,6 +50,8 @@ module HrLite
|
|
|
50
50
|
# (A duplicate live request for the same date cannot exist — the
|
|
51
51
|
# partial unique index on [user_id, date_worked] guarantees it.)
|
|
52
52
|
credit_balance!(type)
|
|
53
|
+
AuditLog.record!(action: "comp_off.approved", subject: self, actor: actor,
|
|
54
|
+
changes: { "date_worked" => date_worked.to_s, "days" => credit_days.to_s("F") })
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
Notifications.publish(
|
|
@@ -79,7 +79,7 @@ module HrLite
|
|
|
79
79
|
def default_visibility_from_category
|
|
80
80
|
return if visibility.present?
|
|
81
81
|
|
|
82
|
-
self.visibility = SENSITIVE_CATEGORIES.include?(category.to_s) ? "money" : "hr"
|
|
82
|
+
self.visibility = SENSITIVE_CATEGORIES.include?(category.to_s.downcase.strip) ? "money" : "hr"
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
def file_is_an_allowed_kind
|
|
@@ -39,6 +39,7 @@ module HrLite
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def approve!(actor:, note: nil)
|
|
42
|
+
raise ActiveRecord::RecordInvalid.new(self), "not submitted" unless submitted?
|
|
42
43
|
return record_routed!(actor, note, :approved) if awaiting?(actor)
|
|
43
44
|
|
|
44
45
|
settle!("approved", actor, note)
|
|
@@ -46,6 +47,7 @@ module HrLite
|
|
|
46
47
|
|
|
47
48
|
def reject!(actor:, note:)
|
|
48
49
|
raise ArgumentError, "a rejection needs a reason" if note.blank?
|
|
50
|
+
raise ActiveRecord::RecordInvalid.new(self), "not submitted" unless submitted?
|
|
49
51
|
return record_routed!(actor, note, :rejected) if awaiting?(actor)
|
|
50
52
|
|
|
51
53
|
settle!("rejected", actor, note)
|
|
@@ -90,6 +92,7 @@ module HrLite
|
|
|
90
92
|
|
|
91
93
|
def settle!(new_status, actor, note)
|
|
92
94
|
update!(status: new_status, decision_note: note.presence)
|
|
95
|
+
approval_route.cancel_all! # decided outside the flow: nobody is left waiting to be asked
|
|
93
96
|
Notifications.publish(
|
|
94
97
|
new_status == "approved" ? "expense.approved" : "expense.rejected",
|
|
95
98
|
title: "Your #{category.name} claim was #{new_status}",
|
|
@@ -30,6 +30,7 @@ module HrLite
|
|
|
30
30
|
def category_label = self.class.category_label(category)
|
|
31
31
|
|
|
32
32
|
def assign!(actor:, assignee:)
|
|
33
|
+
raise ActiveRecord::RecordInvalid.new(self), "already settled" unless open? || in_progress?
|
|
33
34
|
update!(assigned_to_id: assignee.id, status: "in_progress")
|
|
34
35
|
Notifications.publish(
|
|
35
36
|
"hr_request.assigned",
|
|
@@ -41,6 +42,7 @@ module HrLite
|
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
def resolve!(actor:, resolution:)
|
|
45
|
+
raise ActiveRecord::RecordInvalid.new(self), "already settled" unless open? || in_progress?
|
|
44
46
|
raise ArgumentError, "a resolution needs an answer" if resolution.blank?
|
|
45
47
|
|
|
46
48
|
update!(status: "resolved", resolution: resolution, resolved_at: Time.current,
|
data/app/models/hr_lite/kudo.rb
CHANGED
|
@@ -33,10 +33,10 @@ module HrLite
|
|
|
33
33
|
BADGES[badge]
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
# Creates mention rows from the message markers (
|
|
36
|
+
# Creates mention rows from the message markers (employees only,
|
|
37
37
|
# giver excluded) and notifies them. Called once, right after create.
|
|
38
38
|
def register_mentions!
|
|
39
|
-
ids = HrLite::MentionParser.user_ids(message) - [ giver_id ]
|
|
39
|
+
ids = (HrLite::MentionParser.user_ids(message) - [ giver_id ]) & HrLite.employees.map(&:id)
|
|
40
40
|
users = HrLite.user_klass.where(id: ids)
|
|
41
41
|
users.each { |u| kudo_mentions.create!(user_id: u.id) }
|
|
42
42
|
|
|
@@ -5,12 +5,15 @@ module HrLite
|
|
|
5
5
|
STATUSES = %w[pending approved rejected cancelled].freeze
|
|
6
6
|
|
|
7
7
|
belongs_to :user, class_name: HrLite.config.user_class
|
|
8
|
-
belongs_to :leave_type
|
|
8
|
+
belongs_to :leave_type, optional: true # required below, in words ("must exist" is fixed)
|
|
9
9
|
belongs_to :decided_by, class_name: HrLite.config.user_class, optional: true
|
|
10
10
|
# Set when HR records leave the employee forgot to apply for.
|
|
11
11
|
belongs_to :created_by, class_name: HrLite.config.user_class, optional: true
|
|
12
12
|
|
|
13
|
-
validates :
|
|
13
|
+
validates :leave_type, presence: { message: "is missing — pick one" }
|
|
14
|
+
validates :start_date, presence: { message: "is missing — pick the first day" }
|
|
15
|
+
# With no start the start error says it all (a blank end means one day).
|
|
16
|
+
validates :end_date, presence: { message: "is missing — pick the last day" }, if: :start_date
|
|
14
17
|
validates :status, inclusion: { in: STATUSES }
|
|
15
18
|
validates :half_day_part, inclusion: { in: %w[first second] }, allow_blank: true
|
|
16
19
|
|
|
@@ -50,7 +53,10 @@ module HrLite
|
|
|
50
53
|
return 0 if leave_type.unlimited?
|
|
51
54
|
return paid_days ? LeaveDayCounter.count(self) - paid_days : 0 if approved?
|
|
52
55
|
|
|
53
|
-
|
|
56
|
+
# Pending requests of this type and leave year filed first draw on the same balance (warning only).
|
|
57
|
+
queued = self.class.pending.where(user_id: user_id, leave_type_id: leave_type_id, id: ...id,
|
|
58
|
+
start_date: LeaveYear.range(LeaveYear.key_for(start_date))).sum(:days_count)
|
|
59
|
+
[ LeaveDayCounter.count(self) - covered_by(balance.available(as_of: start_date) - queued), 0 ].max
|
|
54
60
|
end
|
|
55
61
|
|
|
56
62
|
# Unpaid share (0, 0.5 or 1) of `date`: the balance covered the leave's
|
|
@@ -113,6 +119,7 @@ module HrLite
|
|
|
113
119
|
# And nobody should still be asked to decide a request that has been
|
|
114
120
|
# called off — it would sit in their inbox for ever.
|
|
115
121
|
approval_route.cancel_all!
|
|
122
|
+
flag_computed_runs! if was_approved
|
|
116
123
|
end
|
|
117
124
|
|
|
118
125
|
Notifications.publish(
|
|
@@ -229,6 +236,15 @@ module HrLite
|
|
|
229
236
|
self.decision_note = note
|
|
230
237
|
save!
|
|
231
238
|
audit!(new_status, actor, note)
|
|
239
|
+
approval_route.cancel_all! # decided outside the flow too: nobody else is still asked
|
|
240
|
+
flag_computed_runs! if approved?
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# A slip computed before this still pays the old days; a recompute clears the note.
|
|
245
|
+
def flag_computed_runs!
|
|
246
|
+
PayrollRun.where(status: "review", period_month: start_date.beginning_of_month..end_date).find_each do |run|
|
|
247
|
+
run.update!(warnings: run.warnings + [ "#{HrLite.display_name(user)}'s leave (#{date_range_label}) changed after compute — recompute" ])
|
|
232
248
|
end
|
|
233
249
|
end
|
|
234
250
|
|
|
@@ -270,8 +286,8 @@ module HrLite
|
|
|
270
286
|
|
|
271
287
|
# Everyone should know a colleague will be away, the moment they apply —
|
|
272
288
|
# bell + email to the whole team (matrix row "leave.team_notice"; hosts can mute either
|
|
273
|
-
# channel). Deliberately excludes the reason: dates are
|
|
274
|
-
# the why is not.
|
|
289
|
+
# channel). Deliberately excludes the reason and the type: dates are
|
|
290
|
+
# team-relevant, the why is not.
|
|
275
291
|
def notify_team
|
|
276
292
|
return if end_date < Date.current # nobody needs to hear who was out
|
|
277
293
|
|
|
@@ -280,7 +296,7 @@ module HrLite
|
|
|
280
296
|
|
|
281
297
|
Notifications.publish(
|
|
282
298
|
"leave.team_notice",
|
|
283
|
-
title: "#{HrLite.display_name(user)} is on leave
|
|
299
|
+
title: "#{HrLite.display_name(user)} is on leave (#{date_range_label})",
|
|
284
300
|
path: "/team",
|
|
285
301
|
bell_to: team,
|
|
286
302
|
email_to: team
|
|
@@ -336,6 +352,9 @@ module HrLite
|
|
|
336
352
|
clash = self.class.where(user_id: user_id, status: %w[pending approved])
|
|
337
353
|
.where.not(id: id)
|
|
338
354
|
.overlapping_range(start_date, end_date)
|
|
355
|
+
# A first half and a second half of the same day do not overlap.
|
|
356
|
+
opposite = { "first" => "second", "second" => "first" }[half_day_part]
|
|
357
|
+
clash = clash.where.not(half_day_part: opposite).or(clash.where(half_day_part: nil)) if opposite
|
|
339
358
|
errors.add(:base, "Leave already overlaps these dates") if clash.exists?
|
|
340
359
|
end
|
|
341
360
|
|
|
@@ -20,6 +20,9 @@ module HrLite
|
|
|
20
20
|
validate :period_is_not_already_paid, on: :create
|
|
21
21
|
# Removing a line from a paid month rewrites a settled payslip just the same.
|
|
22
22
|
before_destroy { throw :abort if paid_month? }
|
|
23
|
+
# A run already computed for the month still shows the old pay.
|
|
24
|
+
after_create { flag_stale_run("added") }
|
|
25
|
+
after_destroy { flag_stale_run("removed") }
|
|
23
26
|
|
|
24
27
|
scope :for_month, ->(month) { where(period_month: month) }
|
|
25
28
|
|
|
@@ -55,5 +58,10 @@ module HrLite
|
|
|
55
58
|
end
|
|
56
59
|
|
|
57
60
|
def paid_month? = SalarySlip.settled.exists?(user_id: user_id, period_month: period_month)
|
|
61
|
+
|
|
62
|
+
def flag_stale_run(verb)
|
|
63
|
+
run = PayrollRun.find_by(period_month: period_month, status: "review")
|
|
64
|
+
run&.update!(warnings: run.warnings + [ "#{HrLite.display_name(user)}'s #{component.label} was #{verb} after compute — recompute" ])
|
|
65
|
+
end
|
|
58
66
|
end
|
|
59
67
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module HrLite
|
|
2
2
|
# Monthly run lifecycle: draft -> processing -> review -> finalized ->
|
|
3
|
-
# published (terminal).
|
|
4
|
-
#
|
|
3
|
+
# published (terminal). The page computes through PayrollComputeJob
|
|
4
|
+
# (compute_later!); compute! itself is synchronous.
|
|
5
5
|
class PayrollRun < ApplicationRecord
|
|
6
6
|
STATUSES = %w[draft processing review finalized published].freeze
|
|
7
7
|
|
|
@@ -12,8 +12,9 @@ module HrLite
|
|
|
12
12
|
belongs_to :first_approved_by, class_name: HrLite.config.user_class, optional: true
|
|
13
13
|
|
|
14
14
|
# A run with one of these is not paid on the 3rd without a person looking:
|
|
15
|
-
# someone missing, a state levying ₹0,
|
|
16
|
-
|
|
15
|
+
# someone missing, a state levying ₹0, last year's / unverified rates, or
|
|
16
|
+
# a change after compute ("… after compute — recompute") the slips lack.
|
|
17
|
+
BLOCKING_WARNING = /\ANo (salary structure|professional-tax slabs)|statutory card|after compute/
|
|
17
18
|
|
|
18
19
|
validates :period_month, presence: true, uniqueness: true
|
|
19
20
|
validates :status, inclusion: { in: STATUSES }
|
|
@@ -37,9 +38,9 @@ module HrLite
|
|
|
37
38
|
# `processing` is allowed back in so a run stranded there by a killed
|
|
38
39
|
# process (deploy roll, timeout) can be recomputed instead of blocking
|
|
39
40
|
# that month forever.
|
|
40
|
-
def compute!(actor:)
|
|
41
|
+
def compute!(actor:, from: status)
|
|
41
42
|
raise_unless %w[draft review processing]
|
|
42
|
-
previous =
|
|
43
|
+
previous = from
|
|
43
44
|
|
|
44
45
|
begin
|
|
45
46
|
update!(status: "processing")
|
|
@@ -61,6 +62,14 @@ module HrLite
|
|
|
61
62
|
end
|
|
62
63
|
end
|
|
63
64
|
|
|
65
|
+
# ~1.2k queries at 54 people: the request only flips the run to processing.
|
|
66
|
+
def compute_later!(actor:)
|
|
67
|
+
raise_unless %w[draft review processing]
|
|
68
|
+
from = status
|
|
69
|
+
update!(status: "processing")
|
|
70
|
+
PayrollComputeJob.perform_later(self, from, actor)
|
|
71
|
+
end
|
|
72
|
+
|
|
64
73
|
def finalize!(actor:)
|
|
65
74
|
raise_unless %w[review]
|
|
66
75
|
raise ActiveRecord::RecordInvalid.new(self), "no slips" if salary_slips.none?
|
|
@@ -135,7 +135,7 @@ module HrLite
|
|
|
135
135
|
transition!("cancelled", actor, nil)
|
|
136
136
|
Notifications.publish(
|
|
137
137
|
"regularization.cancelled",
|
|
138
|
-
title: "#{HrLite.display_name(user)} cancelled a
|
|
138
|
+
title: "#{HrLite.display_name(user)} cancelled a day fix request (#{date.strftime('%d %b')})",
|
|
139
139
|
path: "/admin/regularization_requests",
|
|
140
140
|
bell_to: HrLite.admin_users
|
|
141
141
|
)
|
|
@@ -220,7 +220,7 @@ module HrLite
|
|
|
220
220
|
def notify_requested
|
|
221
221
|
Notifications.publish(
|
|
222
222
|
"regularization.requested",
|
|
223
|
-
title: "#{HrLite.display_name(user)}
|
|
223
|
+
title: "#{HrLite.display_name(user)} asked to fix #{date.strftime('%d %b')} " \
|
|
224
224
|
"(#{times_label})",
|
|
225
225
|
body: reason.presence,
|
|
226
226
|
path: "/admin/regularization_requests/#{id}",
|
|
@@ -20,6 +20,7 @@ module HrLite
|
|
|
20
20
|
STATUSES.each { |s| define_method("#{s}?") { status == s } }
|
|
21
21
|
|
|
22
22
|
after_create :notify_submitted
|
|
23
|
+
after_create { audit!("submitted", user) }
|
|
23
24
|
|
|
24
25
|
def accept!(actor:, last_day: nil, note: nil)
|
|
25
26
|
raise ActiveRecord::RecordInvalid.new(self), "not pending" unless pending?
|
|
@@ -34,6 +35,7 @@ module HrLite
|
|
|
34
35
|
# nothing was stamped, because the screen used to claim it was.
|
|
35
36
|
@exit_date_recorded = profile.present?
|
|
36
37
|
profile&.update!(date_of_exit: final_day)
|
|
38
|
+
audit!("accepted", actor)
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
# Notice periods are the normal case, so access is revoked here only
|
|
@@ -55,7 +57,7 @@ module HrLite
|
|
|
55
57
|
def withdraw!(actor:)
|
|
56
58
|
raise ActiveRecord::RecordInvalid.new(self), "not pending" unless pending? && actor.id == user_id
|
|
57
59
|
|
|
58
|
-
update!(status: "withdrawn", decided_at: Time.current)
|
|
60
|
+
transaction { update!(status: "withdrawn", decided_at: Time.current) && audit!("withdrawn", actor) }
|
|
59
61
|
Notifications.publish(
|
|
60
62
|
"resignation.withdrawn",
|
|
61
63
|
title: "#{HrLite.display_name(user)} withdrew their resignation",
|
|
@@ -72,6 +74,8 @@ module HrLite
|
|
|
72
74
|
|
|
73
75
|
private
|
|
74
76
|
|
|
77
|
+
def audit!(action, actor) = AuditLog.record!(action: "resignation.#{action}", subject: self, actor: actor, changes: { "last_day" => proposed_last_day.to_s })
|
|
78
|
+
|
|
75
79
|
# Best-effort, like every other call of this hook: a host that cannot
|
|
76
80
|
# revoke must not roll back an accepted resignation.
|
|
77
81
|
def revoke_access!
|
data/app/models/hr_lite/role.rb
CHANGED
|
@@ -36,12 +36,17 @@ module HrLite
|
|
|
36
36
|
# permission being taken away, not one left alone.
|
|
37
37
|
def replace_grants!(grants)
|
|
38
38
|
transaction do
|
|
39
|
+
before = grant_map
|
|
39
40
|
role_grants.destroy_all
|
|
40
41
|
grants.each do |key, scope|
|
|
41
42
|
next if scope.blank? || scope.to_s == "none"
|
|
42
43
|
|
|
43
44
|
role_grants.create!(permission_key: Permissions.validate!(key), scope: scope.to_s)
|
|
44
45
|
end
|
|
46
|
+
role_grants.reload
|
|
47
|
+
after = grant_map
|
|
48
|
+
changed = (before.keys | after.keys).reject { |key| before[key] == after[key] }
|
|
49
|
+
AuditLog.record!(action: "role.grants_changed", subject: self, changes: changed.to_h { |key| [ key, [ before[key], after[key] ] ] }) if changed.any?
|
|
45
50
|
end
|
|
46
51
|
reload
|
|
47
52
|
end
|
|
@@ -63,7 +63,7 @@ module HrLite
|
|
|
63
63
|
|
|
64
64
|
# Bump the version when the PDF template changes, or the old layout serves until the cache expires.
|
|
65
65
|
def pdf_cache_key
|
|
66
|
-
[ "
|
|
66
|
+
[ "hr_lite_slip_v3", id, updated_at.to_i, payroll_run.status,
|
|
67
67
|
user_profile&.updated_at.to_i ].join("/")
|
|
68
68
|
end
|
|
69
69
|
|
|
@@ -86,6 +86,11 @@ module HrLite
|
|
|
86
86
|
|
|
87
87
|
def verify!(actor:, note: nil)
|
|
88
88
|
raise ActiveRecord::RecordInvalid.new(self), "not submitted" unless submitted?
|
|
89
|
+
# A blank verified amount would count as ₹0 allowed once verified.
|
|
90
|
+
if tax_declaration_items.any? { |item| item.verified_amount.nil? }
|
|
91
|
+
errors.add(:base, "Every line needs a verified amount — enter 0 where the proof supports nothing")
|
|
92
|
+
raise ActiveRecord::RecordInvalid, self
|
|
93
|
+
end
|
|
89
94
|
|
|
90
95
|
update!(status: "verified", verified_by_id: actor.id, verified_at: Time.current,
|
|
91
96
|
note: note.presence)
|
|
@@ -247,6 +247,10 @@ module HrLite
|
|
|
247
247
|
# Late ladder against slot + buffer; after 11:00 also +1h on the next two
|
|
248
248
|
# working days; after 11:30 the day is a half day. The buffer shifts all three.
|
|
249
249
|
def apply_slot_rules(record)
|
|
250
|
+
# No slot to be late for on a weekly off, a holiday, or leave that starts the day (full or first half).
|
|
251
|
+
return unless WorkingCalendar.for_user(@user.id, record.date..record.date).working_day?(record.date)
|
|
252
|
+
return if LeaveRequest.active_on(record.date).where(user_id: @user.id, half_day_part: [ nil, "", "first" ]).exists?
|
|
253
|
+
|
|
250
254
|
local = record.check_in_at.in_time_zone(HrLite.config.time_zone)
|
|
251
255
|
arrived = local.hour * 60 + local.min - EmployeeProfile.where(user_id: @user.id).pick(:buffer_minutes).to_i
|
|
252
256
|
late = AttendanceSlot.penalty_minutes(arrived - AttendanceSlot.start_minute_for(@user.id, record.date))
|
|
@@ -17,17 +17,6 @@ module HrLite
|
|
|
17
17
|
reach(LeaveRequest.active_on(@date).includes(:leave_type, :user).order(:start_date))
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
# Comp-off and regularization are approvals too. Counting leave alone made
|
|
21
|
-
# the board announce "All present and accounted for" while both queues
|
|
22
|
-
# still had work in them.
|
|
23
|
-
def pending_comp_offs
|
|
24
|
-
reach(CompOffRequest.pending.includes(:user).recent_first)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def pending_regularizations
|
|
28
|
-
reach(RegularizationRequest.pending.includes(:user).recent_first)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
20
|
def flagged_today
|
|
32
21
|
reach(AttendanceRecord.for_date(@date).flagged.includes(:user))
|
|
33
22
|
end
|
|
@@ -41,7 +30,6 @@ module HrLite
|
|
|
41
30
|
|
|
42
31
|
def kpis
|
|
43
32
|
{
|
|
44
|
-
pending: pending_requests.count + pending_comp_offs.count + pending_regularizations.count,
|
|
45
33
|
on_leave: on_leave_today.count,
|
|
46
34
|
flagged: flagged_today.count,
|
|
47
35
|
missing_checkout: missing_checkout_yesterday.count
|
|
@@ -16,8 +16,11 @@ module HrLite
|
|
|
16
16
|
def self.for(user_id, at: Time.current) = for_users([ user_id ], at: at)[user_id]
|
|
17
17
|
|
|
18
18
|
# When an open Busy or Meeting ends, or nil when the person is reachable.
|
|
19
|
-
def self.do_not_disturb_until(user_id, at: Time.current)
|
|
20
|
-
|
|
19
|
+
def self.do_not_disturb_until(user_id, at: Time.current) = do_not_disturb_for([ user_id ], at: at)[user_id]
|
|
20
|
+
|
|
21
|
+
# The same for many people in one query: { user_id => ends_at }, reachable people absent.
|
|
22
|
+
def self.do_not_disturb_for(user_ids, at: Time.current)
|
|
23
|
+
WorkModeEvent.open.overlay.where(user_id: user_ids, mode: %w[busy meeting]).where("ends_at > ?", at).group(:user_id).maximum(:ends_at)
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
def self.for_users(user_ids, at: Time.current)
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
<% end %>
|
|
23
23
|
|
|
24
24
|
<section class="hrl-card">
|
|
25
|
+
<% if @assets.empty? %>
|
|
26
|
+
<p class="hrl-muted">No assets recorded yet. Add the first one above.</p>
|
|
27
|
+
<% else %>
|
|
25
28
|
<div class="hrl-table-wrap">
|
|
26
29
|
<table class="hrl-table hrl-table--stack">
|
|
27
30
|
<thead><tr><th>Asset</th><th>Status</th><th>With</th><th></th></tr></thead>
|
|
@@ -39,13 +42,16 @@
|
|
|
39
42
|
<td data-label="">
|
|
40
43
|
<% if asset.assigned? %>
|
|
41
44
|
<%= form_with url: hr_lite.take_back_admin_asset_path(asset), method: :post do %>
|
|
42
|
-
<%=
|
|
45
|
+
<%= label_tag "asset-condition-#{asset.id}", "Condition on return", class: "hrl-sr-only" %>
|
|
46
|
+
<%= text_field_tag :condition_note, nil, id: "asset-condition-#{asset.id}", placeholder: "Condition on return", class: "hrl-input" %>
|
|
43
47
|
<%= submit_tag "Take back", class: "hrl-btn" %>
|
|
44
48
|
<% end %>
|
|
45
49
|
<% elsif asset.available? %>
|
|
46
50
|
<%= form_with url: hr_lite.assign_admin_asset_path(asset), method: :post do %>
|
|
51
|
+
<%= label_tag "asset-holder-#{asset.id}", "Hand over to", class: "hrl-sr-only" %>
|
|
47
52
|
<%= select_tag :user_id,
|
|
48
|
-
options_for_select(HrLite.employees.map { |u| [ hr_display_name(u), u.id ] }),
|
|
53
|
+
options_for_select(HrLite.employees.map { |u| [ hr_display_name(u), u.id ] }), id: "asset-holder-#{asset.id}",
|
|
54
|
+
include_blank: "Choose…", required: true, class: "hrl-input",
|
|
49
55
|
data: { hrl_filter: "Type a name to filter" } %>
|
|
50
56
|
<%= submit_tag "Hand over", class: "hrl-btn" %>
|
|
51
57
|
<% end %>
|
|
@@ -56,5 +62,6 @@
|
|
|
56
62
|
</tbody>
|
|
57
63
|
</table>
|
|
58
64
|
</div>
|
|
65
|
+
<% end %>
|
|
59
66
|
<%= hrl_pagination %>
|
|
60
67
|
</section>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<% content_for(:page_title) { "Attendance — #{hr_display_name(@employee)}" } %>
|
|
2
2
|
<% content_for(:tabrow) { render "hr_lite/shared/person_tabs", user: @employee } %>
|
|
3
|
+
<% content_for(:back, "Team") %>
|
|
3
4
|
<div class="hrl-page__head">
|
|
4
5
|
<h1 class="hrl-page__title"><%= hr_display_name(@employee) %></h1>
|
|
5
6
|
<div class="hrl-page__actions">
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
<% if @punches.any? %>
|
|
34
35
|
<section class="hrl-card">
|
|
35
36
|
<h2 class="hrl-card__title">Where they punched</h2>
|
|
37
|
+
<% all_punches = HrLite::AttendancePunch.where(user_id: @employee.id, date: @punches.map(&:date)).order(:at, :id).group_by(&:date) # one query, not one a day %>
|
|
36
38
|
<% @punches.each do |record| %>
|
|
37
39
|
<div class="hrl-feed__item hrl-small">
|
|
38
40
|
<div class="hrl-feed__body">
|
|
@@ -47,7 +49,7 @@
|
|
|
47
49
|
<% end %>
|
|
48
50
|
</div>
|
|
49
51
|
<% end %>
|
|
50
|
-
<% if record.check_in_place %><%= render "hr_lite/attendance/trail", record: record %><% end %>
|
|
52
|
+
<% if record.check_in_place %><%= render "hr_lite/attendance/trail", record: record, punches: all_punches.fetch(record.date, []) %><% end %>
|
|
51
53
|
<% if (gap = record.punch_gap_m) %>
|
|
52
54
|
<div class="<%= gap > HrLite::AttendanceRecord::PUNCH_GAP_ALERT_M ? "hrl-badge hrl-badge--warn" : "hrl-muted" %>"><%= (gap / 1000.0).round(1) %> km apart</div>
|
|
53
55
|
<% end %>
|
|
@@ -25,14 +25,17 @@
|
|
|
25
25
|
<% end %>
|
|
26
26
|
</section>
|
|
27
27
|
|
|
28
|
+
<% fixes_pending = HrLite::RegularizationRequest.pending.where(user_id: @employee.id, date: @period.from..@period.to).pluck(:date) %>
|
|
28
29
|
<section class="hrl-card">
|
|
29
30
|
<h2 class="hrl-card__title">Days</h2>
|
|
30
31
|
<% (@period.from..@period.to).each do |date| %>
|
|
31
|
-
<% day = @days.for(date); rec = day.record; punches = @punches[date].to_a
|
|
32
|
+
<% day = @days.for(date); rec = day.record; punches = @punches[date].to_a
|
|
33
|
+
not_in_yet = date == Date.current && day.kind == :absent # the day is not over %>
|
|
32
34
|
<div class="hrl-feed__item hrl-row">
|
|
33
35
|
<div class="hrl-feed__body hrl-small">
|
|
34
36
|
<strong><%= date.strftime("%a %-d %b") %></strong>
|
|
35
|
-
<span class="hrl-badge <%= tones[day.kind] %>"><%= HrLite::ApplicationHelper::DAY_STATES[day.kind][1] %></span>
|
|
37
|
+
<span class="hrl-badge <%= not_in_yet ? "hrl-badge--muted" : tones[day.kind] %>"><%= not_in_yet ? "Not in yet" : HrLite::ApplicationHelper::DAY_STATES[day.kind][1] %></span>
|
|
38
|
+
<% if fixes_pending.include?(date) %><span class="hrl-badge hrl-badge--warn">Fix pending</span><% end %>
|
|
36
39
|
<% if @profile&.hours_allowance_minutes && (length = HrLite::EmployeeProfile.day_minutes_for(@employee.id, on: date)) < HrLite::Setting.instance.day_minutes %>
|
|
37
40
|
<span class="hrl-badge hrl-badge--muted">Day <%= hrl_duration(length * 60) %></span>
|
|
38
41
|
<% end %>
|