hr_lite 0.2.1 → 0.4.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 +96 -0
- data/README.md +2 -1
- data/app/assets/stylesheets/hr_lite/hr_lite.css +35 -4
- data/app/controllers/hr_lite/admin/comp_off_requests_controller.rb +38 -0
- data/app/controllers/hr_lite/admin/employees_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/leave_balances_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/leave_types_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/regularization_requests_controller.rb +38 -0
- data/app/controllers/hr_lite/comp_off_requests_controller.rb +46 -0
- data/app/controllers/hr_lite/leave_balances_controller.rb +1 -1
- data/app/controllers/hr_lite/leave_requests_controller.rb +1 -1
- data/app/controllers/hr_lite/org_controller.rb +51 -0
- data/app/controllers/hr_lite/regularization_requests_controller.rb +36 -0
- data/app/controllers/hr_lite/team_controller.rb +11 -0
- data/app/helpers/hr_lite/application_helper.rb +62 -3
- data/app/jobs/hr_lite/leave_year_rollover_job.rb +18 -6
- data/app/models/hr_lite/comp_off_request.rb +177 -0
- data/app/models/hr_lite/employee_profile.rb +55 -0
- data/app/models/hr_lite/leave_balance.rb +43 -10
- data/app/models/hr_lite/leave_request.rb +23 -5
- data/app/models/hr_lite/leave_type.rb +17 -0
- data/app/models/hr_lite/regularization_request.rb +181 -0
- data/app/services/hr_lite/team_day.rb +84 -0
- data/app/views/hr_lite/admin/comp_off_requests/index.html.erb +37 -0
- data/app/views/hr_lite/admin/comp_off_requests/show.html.erb +47 -0
- data/app/views/hr_lite/admin/employees/_form.html.erb +7 -0
- data/app/views/hr_lite/admin/employees/show.html.erb +1 -0
- data/app/views/hr_lite/admin/leave_balances/index.html.erb +1 -1
- data/app/views/hr_lite/admin/leave_requests/index.html.erb +2 -0
- data/app/views/hr_lite/admin/leave_types/_form.html.erb +4 -0
- data/app/views/hr_lite/admin/regularization_requests/index.html.erb +37 -0
- data/app/views/hr_lite/admin/regularization_requests/show.html.erb +50 -0
- data/app/views/hr_lite/admin/shared/_approvals_tabs.html.erb +11 -0
- data/app/views/hr_lite/attendance/show.html.erb +3 -0
- data/app/views/hr_lite/comp_off_requests/index.html.erb +42 -0
- data/app/views/hr_lite/comp_off_requests/new.html.erb +28 -0
- data/app/views/hr_lite/employee_profiles/show.html.erb +3 -0
- data/app/views/hr_lite/leave_balances/index.html.erb +1 -1
- data/app/views/hr_lite/leave_requests/_balance_chips.html.erb +2 -2
- data/app/views/hr_lite/leave_requests/index.html.erb +1 -0
- data/app/views/hr_lite/org/_node.html.erb +14 -0
- data/app/views/hr_lite/org/show.html.erb +38 -0
- data/app/views/hr_lite/regularization_requests/index.html.erb +41 -0
- data/app/views/hr_lite/regularization_requests/new.html.erb +30 -0
- data/app/views/hr_lite/team/show.html.erb +48 -0
- data/app/views/layouts/hr_lite/application.html.erb +1 -1
- data/config/routes.rb +14 -0
- data/db/migrate/20260719104315_add_comp_off_to_hr_lite_leave_types.rb +6 -0
- data/db/migrate/20260719104316_create_hr_lite_comp_off_requests.rb +24 -0
- data/db/migrate/20260719104317_create_hr_lite_regularization_requests.rb +20 -0
- data/db/migrate/20260719162154_add_manager_to_hr_lite_employee_profiles.rb +8 -0
- data/lib/hr_lite/configuration.rb +14 -0
- data/lib/hr_lite/leave_year.rb +33 -0
- data/lib/hr_lite/notifications.rb +14 -2
- data/lib/hr_lite/seeds.rb +16 -1
- data/lib/hr_lite/version.rb +1 -1
- data/lib/hr_lite.rb +8 -0
- metadata +27 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d95fb8f07ae1876be368009a6d539b30938e7635d95abcbe9e1bbd404e57d0c
|
|
4
|
+
data.tar.gz: a3f31cbd6ca7936381ce13f3baf2c4ba6532431f5066867345a1c6e4c30a69b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8efffc3c3f5468f3e15c7118ce0f50248e74a1e0b359a009022b9c40376c5b41c755c4cb350fbd68a6d90d734acfa23989f790c0143f2f815440983c52425488
|
|
7
|
+
data.tar.gz: b020fd0ca4df79fba65e8efdc7a99da0c6979ec050405b7b9b1fe63091d62cd421725815f46f03da450edf82635336eb7ac8814ce35807f2369252bb18631bd8
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,102 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-07-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Org chart (`/org`)**: everyone-visible reporting tree (who reports to
|
|
15
|
+
whom, names + designations + departments only — never salary or private
|
|
16
|
+
data) plus each viewer's own reporting line labelled L1/L2/... Managers
|
|
17
|
+
are set per employee by leadership ("Reports to"); reporting loops are
|
|
18
|
+
rejected.
|
|
19
|
+
- **Configurable leave year** (`config.leave_year_start_month`, default 1):
|
|
20
|
+
set 7 for a July–June leave year. Balances, accrual, the year-boundary
|
|
21
|
+
split rule, carry-forward rollover and comp-off credits all follow it.
|
|
22
|
+
Balance headings show "2026–27"-style labels for non-calendar years.
|
|
23
|
+
- **Joining-date proration, Keka-style**: entitlement accrues only from
|
|
24
|
+
the month someone joins (joined on/before the 15th → that month counts;
|
|
25
|
+
after → from the next month). Applies to monthly accrual AND
|
|
26
|
+
yearly-upfront grants (upfront = remaining months × monthly rate).
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `LeaveBalance#year` is now a LeaveYear key; `LeaveYearRolloverJob`
|
|
31
|
+
defaults to the current leave year in the configured HR time zone
|
|
32
|
+
(schedule it on the leave year's first day). The request-split
|
|
33
|
+
validation message says "leave-year boundary".
|
|
34
|
+
|
|
35
|
+
### Upgrade notes
|
|
36
|
+
|
|
37
|
+
- **Set `leave_year_start_month` once, at install time.** Balance rows
|
|
38
|
+
are keyed by leave year with no stored epoch — changing the start
|
|
39
|
+
month on an install with existing balances silently reinterprets
|
|
40
|
+
every row (carry, adjustments, comp-off credits) and miskeys
|
|
41
|
+
historical requests. The setter validates 1..12 and accepts "7".
|
|
42
|
+
- **Joining-date proration applies to existing data.** Employees who
|
|
43
|
+
joined partway through the CURRENT year previously showed the full
|
|
44
|
+
year's accrual; from 0.4.0 they accrue only from their joining month,
|
|
45
|
+
so their entitlement can drop (and, if they already used more, go
|
|
46
|
+
negative). Where the old number was intended, add a one-off balance
|
|
47
|
+
adjustment with a note.
|
|
48
|
+
|
|
49
|
+
## [0.3.0] - 2026-07-19
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- **Comp-off requests**: employees request a credit for working a weekend
|
|
54
|
+
or holiday; admin approval credits the comp-off leave type's balance
|
|
55
|
+
(mark the type under Settings — seeds flag `CO`). New events
|
|
56
|
+
`comp_off.requested/approved/rejected/cancelled`.
|
|
57
|
+
- **Regularization tickets**: forgot to punch? Employees propose the actual
|
|
58
|
+
times with a reason; admin approval writes them onto the day's attendance
|
|
59
|
+
record with the full regularization trail. New events
|
|
60
|
+
`regularization.requested/approved/rejected/cancelled`.
|
|
61
|
+
- **Team board (`/team`)**: everyone-visible who's-in/who's-out for any
|
|
62
|
+
date — punch times, leave badges, hours worked that day and that month.
|
|
63
|
+
- **Team leave notices**: approving a leave now bells + emails the whole
|
|
64
|
+
team ("X is on leave …", matrix row `leave.team_notice`; reasons are
|
|
65
|
+
never broadcast).
|
|
66
|
+
- Approvals screens grew Leaves / Comp-off / Regularization tabs with
|
|
67
|
+
pending counts.
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- Checkboxes rendered 100%-wide with misplaced labels (the bare
|
|
72
|
+
`input[type]` width rule out-ranked `.hrl-field--check`); checkbox rows
|
|
73
|
+
now size naturally and pick up the accent colour.
|
|
74
|
+
- Hardening from adversarial review: comp-off credits land in the year
|
|
75
|
+
they can be spent (a December Sunday approved in January no longer
|
|
76
|
+
strands the credit on last year's balance); the balance increment locks
|
|
77
|
+
the balance row (no lost update when two admins approve concurrently);
|
|
78
|
+
a partial unique index guarantees one live comp-off request per person
|
|
79
|
+
per date; approval re-checks the calendar (StaleOffDay) after holiday
|
|
80
|
+
edits; regularization approval refuses merges that would corrupt the
|
|
81
|
+
record (checkout with no check-in, checkout before the genuine
|
|
82
|
+
check-in) with the real reason surfaced to the admin, keeps GPS flags,
|
|
83
|
+
and writes an AuditLog row like the manual fix path; tickets cannot
|
|
84
|
+
target a day covered by approved leave; only one leave type can carry
|
|
85
|
+
the comp-off flag; team notices skip exited staff; a host
|
|
86
|
+
notification-matrix override pinned on an older version no longer
|
|
87
|
+
silently drops events it doesn't know (defaults merge underneath).
|
|
88
|
+
|
|
89
|
+
### Changed
|
|
90
|
+
|
|
91
|
+
- `Seeds.run!` no longer re-flags CO as comp-off on every deploy (an
|
|
92
|
+
operator disabling comp-off stays disabled); pre-0.3.0 installs run
|
|
93
|
+
`Seeds.seed_comp_off_flag!` once or tick the box under Settings.
|
|
94
|
+
|
|
95
|
+
## [0.2.2] - 2026-07-19
|
|
96
|
+
|
|
97
|
+
### Fixed
|
|
98
|
+
|
|
99
|
+
- Link-styled controls were unreadable: the global `.hrl-body a` colour
|
|
100
|
+
rule out-ranked component classes, so primary action links ("New
|
|
101
|
+
structure", "New run", "Add office", "Apply", "Download PDF", active
|
|
102
|
+
filter chips) rendered accent-on-accent with invisible text, and the
|
|
103
|
+
side-nav links lost their muted colour. The rule is now wrapped in
|
|
104
|
+
`:where()` (zero specificity) so every `.hrl-*` component wins.
|
|
105
|
+
|
|
10
106
|
## [0.2.1] - 2026-07-19
|
|
11
107
|
|
|
12
108
|
### Added
|
data/README.md
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
[](MIT-LICENSE)
|
|
6
6
|
|
|
7
7
|
A lightweight, self-contained HRMS engine for Rails — attendance with
|
|
8
|
-
geolocation, leave management
|
|
8
|
+
geolocation, leave management with comp-off requests, regularization tickets,
|
|
9
|
+
an everyone-visible team board, holiday calendar, full Indian payroll
|
|
9
10
|
(PF/ESI/PT/TDS) with salary-slip PDFs, kudos with @mentions, appraisals and
|
|
10
11
|
promotions. Think Keka-lite, mounted inside your existing app in a few lines.
|
|
11
12
|
|
|
@@ -32,7 +32,10 @@
|
|
|
32
32
|
-webkit-font-smoothing: antialiased;
|
|
33
33
|
overflow-x: hidden;
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
/* :where() keeps this at zero specificity so any component class
|
|
36
|
+
(.hrl-btn--primary, .hrl-nav__link, ...) can set its own link colour —
|
|
37
|
+
a plain `.hrl-body a` would out-rank them and paint accent-on-accent. */
|
|
38
|
+
:where(.hrl-body) a { color: var(--hrl-accent); text-decoration: none; }
|
|
36
39
|
.hrl-shell { display: flex; min-height: 100vh; }
|
|
37
40
|
.hrl-content { flex: 1; min-width: 0; }
|
|
38
41
|
.hrl-main {
|
|
@@ -170,20 +173,30 @@
|
|
|
170
173
|
.hrl-table--stack td { display: flex; justify-content: space-between; gap: .8rem; border-bottom: 1px dashed var(--hrl-line); }
|
|
171
174
|
.hrl-table--stack td::before { content: attr(data-label); color: var(--hrl-muted); font-size: .78rem; }
|
|
172
175
|
.hrl-table--stack td.hrl-num { text-align: left; }
|
|
176
|
+
.hrl-table--stack td.hrl-cell--void { display: none; }
|
|
173
177
|
}
|
|
174
178
|
|
|
175
179
|
/* Forms */
|
|
176
180
|
.hrl-field { margin-bottom: .8rem; }
|
|
177
181
|
.hrl-field label { display: block; font-size: .82rem; font-weight: 600; margin-bottom: .3rem; }
|
|
178
182
|
.hrl-field .hrl-hint { color: var(--hrl-muted); font-size: .76rem; margin-top: .25rem; }
|
|
179
|
-
|
|
183
|
+
/* Full-width text-ish controls only — checkboxes/radios/hidden must keep
|
|
184
|
+
their natural size (a bare [type] selector out-ranked .hrl-field--check
|
|
185
|
+
and blew checkboxes up to 100% width). */
|
|
186
|
+
.hrl-input,
|
|
187
|
+
.hrl-field input[type]:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]),
|
|
188
|
+
.hrl-field select, .hrl-field textarea {
|
|
180
189
|
width: 100%; min-height: 44px; padding: .55rem .7rem; font: inherit;
|
|
181
190
|
border: 1px solid var(--hrl-line); border-radius: var(--hrl-radius-sm);
|
|
182
191
|
background: var(--hrl-card); color: var(--hrl-ink);
|
|
183
192
|
}
|
|
184
193
|
.hrl-field textarea { min-height: 90px; resize: vertical; }
|
|
185
|
-
.hrl-field--check { display: flex; align-items: center; gap: .
|
|
186
|
-
.hrl-field--check input {
|
|
194
|
+
.hrl-field--check { display: flex; align-items: center; gap: .55rem; min-height: 44px; }
|
|
195
|
+
.hrl-field--check input {
|
|
196
|
+
width: 1.15rem; height: 1.15rem; min-height: 0; flex: 0 0 auto;
|
|
197
|
+
accent-color: var(--hrl-accent);
|
|
198
|
+
}
|
|
199
|
+
.hrl-field--check label { margin-bottom: 0; font-weight: 600; }
|
|
187
200
|
.hrl-errors { background: var(--hrl-bad-bg); color: var(--hrl-bad); border-radius: var(--hrl-radius-sm); padding: .7rem .9rem; margin-bottom: .85rem; font-size: .86rem; }
|
|
188
201
|
.hrl-errors ul { margin: .3rem 0 0; padding-left: 1.1rem; }
|
|
189
202
|
.hrl-form-actions { display: flex; gap: .6rem; margin-top: 1rem; }
|
|
@@ -228,6 +241,24 @@
|
|
|
228
241
|
.hrl-pager { display: flex; align-items: center; justify-content: center; gap: .8rem; margin: 1rem 0; font-size: .88rem; }
|
|
229
242
|
.hrl-pager__info { color: var(--hrl-muted); }
|
|
230
243
|
|
|
244
|
+
/* Org chart tree + reporting-line chain */
|
|
245
|
+
.hrl-tree { list-style: none; margin: 0; padding-left: 0; }
|
|
246
|
+
.hrl-tree .hrl-tree { padding-left: 1.1rem; border-left: 2px solid var(--hrl-line); margin-left: .55rem; }
|
|
247
|
+
.hrl-tree__node { margin: .3rem 0; }
|
|
248
|
+
.hrl-tree__card {
|
|
249
|
+
display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: .45rem;
|
|
250
|
+
background: var(--hrl-bg); border: 1px solid var(--hrl-line);
|
|
251
|
+
border-radius: var(--hrl-radius-sm); padding: .4rem .7rem; max-width: 100%;
|
|
252
|
+
}
|
|
253
|
+
.hrl-tree__card--me { border-color: var(--hrl-accent); background: var(--hrl-info-bg); }
|
|
254
|
+
.hrl-tree__name, .hrl-tree__role { min-width: 0; overflow-wrap: anywhere; }
|
|
255
|
+
.hrl-tree__name { font-weight: 650; font-size: .9rem; }
|
|
256
|
+
.hrl-tree__role { color: var(--hrl-muted); font-size: .8rem; }
|
|
257
|
+
.hrl-chainrow { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
|
|
258
|
+
.hrl-chainrow__step { display: inline-flex; align-items: center; gap: .4rem; min-width: 0; }
|
|
259
|
+
.hrl-chainrow .hrl-badge { white-space: normal; overflow-wrap: anywhere; }
|
|
260
|
+
.hrl-chainrow__arrow { color: var(--hrl-muted); }
|
|
261
|
+
|
|
231
262
|
/* Definition list */
|
|
232
263
|
.hrl-deflist { display: grid; grid-template-columns: minmax(7rem, auto) 1fr; gap: .35rem .9rem; font-size: .9rem; }
|
|
233
264
|
.hrl-deflist dt { color: var(--hrl-muted); }
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
class CompOffRequestsController < BaseController
|
|
4
|
+
def index
|
|
5
|
+
@status = params[:status].presence_in(CompOffRequest::STATUSES) || "pending"
|
|
6
|
+
@requests = paginate(CompOffRequest.includes(:user).where(status: @status).recent_first)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def show
|
|
10
|
+
@request = CompOffRequest.find(params[:id])
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def approve
|
|
14
|
+
request = CompOffRequest.find(params[:id])
|
|
15
|
+
request.approve!(actor: hr_current_user, note: params[:decision_note].presence)
|
|
16
|
+
redirect_to admin_comp_off_requests_path,
|
|
17
|
+
notice: "Approved — #{request.credit_days.to_f} day credited."
|
|
18
|
+
rescue CompOffRequest::MissingCompOffType, CompOffRequest::StaleOffDay => e
|
|
19
|
+
redirect_to admin_comp_off_request_path(request), alert: e.message
|
|
20
|
+
rescue ActiveRecord::RecordInvalid
|
|
21
|
+
redirect_to admin_comp_off_request_path(request), alert: "Only pending requests can be decided."
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def reject
|
|
25
|
+
request = CompOffRequest.find(params[:id])
|
|
26
|
+
note = params[:decision_note].to_s.strip
|
|
27
|
+
if note.blank?
|
|
28
|
+
return redirect_to admin_comp_off_request_path(request), alert: "A note is required to reject."
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
request.reject!(actor: hr_current_user, note: note)
|
|
32
|
+
redirect_to admin_comp_off_requests_path, notice: "Request rejected."
|
|
33
|
+
rescue ActiveRecord::RecordInvalid
|
|
34
|
+
redirect_to admin_comp_off_request_path(request), alert: "Only pending requests can be decided."
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -93,7 +93,7 @@ module HrLite
|
|
|
93
93
|
|
|
94
94
|
def profile_params
|
|
95
95
|
params.require(:employee_profile).permit(
|
|
96
|
-
:user_id, :employee_code, :designation, :date_of_birth, :date_of_joining,
|
|
96
|
+
:user_id, :manager_id, :employee_code, :designation, :date_of_birth, :date_of_joining,
|
|
97
97
|
:date_of_exit, :department, :work_location, :pan_number, :pf_uan, :esi_number,
|
|
98
98
|
:bank_account_number, :bank_ifsc, :bank_name, :tax_regime, :declared_annual_deductions
|
|
99
99
|
)
|
|
@@ -3,7 +3,7 @@ module HrLite
|
|
|
3
3
|
class LeaveBalancesController < BaseController
|
|
4
4
|
def index
|
|
5
5
|
year = params[:year].to_i
|
|
6
|
-
@year = year.between?(2000, 2100) ? year :
|
|
6
|
+
@year = year.between?(2000, 2100) ? year : LeaveYear.current_key
|
|
7
7
|
@types = LeaveType.active.where(paid: true).where.not(annual_quota: nil)
|
|
8
8
|
@employees = HrLite.employees
|
|
9
9
|
end
|
|
@@ -46,7 +46,7 @@ module HrLite
|
|
|
46
46
|
|
|
47
47
|
def leave_type_params
|
|
48
48
|
params.require(:leave_type)
|
|
49
|
-
.permit(:name, :code, :color, :paid, :annual_quota, :accrual,
|
|
49
|
+
.permit(:name, :code, :color, :paid, :comp_off, :annual_quota, :accrual,
|
|
50
50
|
:carry_forward_cap, :active, :position)
|
|
51
51
|
end
|
|
52
52
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
class RegularizationRequestsController < BaseController
|
|
4
|
+
def index
|
|
5
|
+
@status = params[:status].presence_in(RegularizationRequest::STATUSES) || "pending"
|
|
6
|
+
@requests = paginate(RegularizationRequest.includes(:user).where(status: @status).recent_first)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def show
|
|
10
|
+
@request = RegularizationRequest.find(params[:id])
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def approve
|
|
14
|
+
request = RegularizationRequest.find(params[:id])
|
|
15
|
+
request.approve!(actor: hr_current_user, note: params[:decision_note].presence)
|
|
16
|
+
redirect_to admin_regularization_requests_path, notice: "Ticket approved — attendance fixed."
|
|
17
|
+
rescue RegularizationRequest::InvalidMerge => e
|
|
18
|
+
redirect_to admin_regularization_request_path(request),
|
|
19
|
+
alert: "Cannot apply — #{e.message}. Fix the day manually or reject with a note."
|
|
20
|
+
rescue ActiveRecord::RecordInvalid
|
|
21
|
+
redirect_to admin_regularization_request_path(request), alert: "Only pending tickets can be decided."
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def reject
|
|
25
|
+
request = RegularizationRequest.find(params[:id])
|
|
26
|
+
note = params[:decision_note].to_s.strip
|
|
27
|
+
if note.blank?
|
|
28
|
+
return redirect_to admin_regularization_request_path(request), alert: "A note is required to reject."
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
request.reject!(actor: hr_current_user, note: note)
|
|
32
|
+
redirect_to admin_regularization_requests_path, notice: "Ticket rejected."
|
|
33
|
+
rescue ActiveRecord::RecordInvalid
|
|
34
|
+
redirect_to admin_regularization_request_path(request), alert: "Only pending tickets can be decided."
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Employee-side comp-off: request a credit for working a weekend/holiday.
|
|
3
|
+
# Scoping to hr_current_user IS the authorization.
|
|
4
|
+
class CompOffRequestsController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@requests = paginate(CompOffRequest.where(user_id: hr_current_user.id).recent_first)
|
|
7
|
+
@comp_off_type = LeaveType.comp_off_type
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def new
|
|
11
|
+
@request = CompOffRequest.new(date_worked: default_date)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create
|
|
15
|
+
@request = CompOffRequest.new(request_params.merge(user_id: hr_current_user.id))
|
|
16
|
+
if @request.save
|
|
17
|
+
redirect_to comp_off_requests_path, notice: "Comp-off request sent for approval."
|
|
18
|
+
else
|
|
19
|
+
render :new, status: :unprocessable_entity
|
|
20
|
+
end
|
|
21
|
+
rescue ActiveRecord::RecordNotUnique
|
|
22
|
+
@request.errors.add(:date_worked, "already has a comp-off request")
|
|
23
|
+
render :new, status: :unprocessable_entity
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def cancel
|
|
27
|
+
request = CompOffRequest.where(user_id: hr_current_user.id).find(params[:id])
|
|
28
|
+
request.cancel!(actor: hr_current_user)
|
|
29
|
+
redirect_to comp_off_requests_path, notice: "Request cancelled."
|
|
30
|
+
rescue ActiveRecord::RecordInvalid
|
|
31
|
+
redirect_to comp_off_requests_path, alert: "Only pending requests can be cancelled."
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def request_params
|
|
37
|
+
params.require(:comp_off_request).permit(:date_worked, :half_day, :reason)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Most recent non-working day — the day they most likely worked extra.
|
|
41
|
+
def default_date
|
|
42
|
+
calendar = WorkingCalendar.new((Date.current - 14)..Date.current)
|
|
43
|
+
(0..14).map { |i| Date.current - i }.find { |d| !calendar.working_day?(d) } || Date.current
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -2,7 +2,7 @@ module HrLite
|
|
|
2
2
|
class LeaveBalancesController < ApplicationController
|
|
3
3
|
def index
|
|
4
4
|
year = params[:year].to_i
|
|
5
|
-
@year = year.between?(2000, 2100) ? year :
|
|
5
|
+
@year = year.between?(2000, 2100) ? year : LeaveYear.current_key
|
|
6
6
|
@balances = LeaveType.active.where(paid: true).where.not(annual_quota: nil).map do |type|
|
|
7
7
|
LeaveBalance.for(hr_current_user, type, @year)
|
|
8
8
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# The everyone-visible org chart: who reports to whom, plus the viewer's
|
|
3
|
+
# own reporting line labelled L1/L2/... Only names, designations and
|
|
4
|
+
# departments — never salary, identity numbers or any other private data.
|
|
5
|
+
class OrgController < ApplicationController
|
|
6
|
+
def show
|
|
7
|
+
profiles = EmployeeProfile.includes(:user)
|
|
8
|
+
.where("date_of_exit IS NULL OR date_of_exit >= ?", Date.current)
|
|
9
|
+
.to_a
|
|
10
|
+
@by_user = profiles.index_by(&:user_id)
|
|
11
|
+
build_tree(profiles)
|
|
12
|
+
@own_profile = @by_user[hr_current_user.id]
|
|
13
|
+
exited = EmployeeProfile.where(date_of_exit: ...Date.current).pluck(:user_id).to_set
|
|
14
|
+
@own_chain = (@own_profile&.reporting_chain || []).reject { |boss| exited.include?(boss.id) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# BFS from the natural roots, then promote anything unreached (a
|
|
20
|
+
# manager cycle that slipped past validation must degrade to extra
|
|
21
|
+
# roots, not silently vanish). @tree_children only ever contains
|
|
22
|
+
# forward edges, so the recursive partial always terminates and every
|
|
23
|
+
# active profile renders exactly once.
|
|
24
|
+
def build_tree(profiles)
|
|
25
|
+
children = profiles.group_by(&:manager_id)
|
|
26
|
+
@roots = profiles.select { |p| p.manager_id.nil? || !@by_user.key?(p.manager_id) }
|
|
27
|
+
.sort_by { |p| HrLite.display_name(p.user).downcase }
|
|
28
|
+
@tree_children = Hash.new { |hash, key| hash[key] = [] }
|
|
29
|
+
visited = {}
|
|
30
|
+
walk = lambda do |node|
|
|
31
|
+
(children[node.user_id] || []).each do |child|
|
|
32
|
+
next if visited[child.user_id]
|
|
33
|
+
|
|
34
|
+
visited[child.user_id] = true
|
|
35
|
+
@tree_children[node.user_id] << child
|
|
36
|
+
walk.call(child)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
@roots.each { |root| visited[root.user_id] = true }
|
|
40
|
+
@roots.each { |root| walk.call(root) }
|
|
41
|
+
|
|
42
|
+
profiles.sort_by(&:id).each do |profile|
|
|
43
|
+
next if visited[profile.user_id]
|
|
44
|
+
|
|
45
|
+
visited[profile.user_id] = true
|
|
46
|
+
@roots << profile
|
|
47
|
+
walk.call(profile)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Employee-side regularization tickets: propose punch times for a day you
|
|
3
|
+
# forgot to check in/out. Scoping to hr_current_user IS the authorization.
|
|
4
|
+
class RegularizationRequestsController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@requests = paginate(RegularizationRequest.where(user_id: hr_current_user.id).recent_first)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def new
|
|
10
|
+
@request = RegularizationRequest.new(date: parse_date_param(params[:date]))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create
|
|
14
|
+
@request = RegularizationRequest.new(request_params.merge(user_id: hr_current_user.id))
|
|
15
|
+
if @request.save
|
|
16
|
+
redirect_to regularization_requests_path, notice: "Ticket raised — an admin will review it."
|
|
17
|
+
else
|
|
18
|
+
render :new, status: :unprocessable_entity
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def cancel
|
|
23
|
+
request = RegularizationRequest.where(user_id: hr_current_user.id).find(params[:id])
|
|
24
|
+
request.cancel!(actor: hr_current_user)
|
|
25
|
+
redirect_to regularization_requests_path, notice: "Ticket cancelled."
|
|
26
|
+
rescue ActiveRecord::RecordInvalid
|
|
27
|
+
redirect_to regularization_requests_path, alert: "Only pending tickets can be cancelled."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def request_params
|
|
33
|
+
params.require(:regularization_request).permit(:date, :check_in_at, :check_out_at, :reason)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# The everyone-visible team board: who's in, who's out, who's on leave,
|
|
3
|
+
# hours worked — for any date. Transparency by design; shows punch times
|
|
4
|
+
# and leave types only, never reasons or personal data.
|
|
5
|
+
class TeamController < ApplicationController
|
|
6
|
+
def show
|
|
7
|
+
@date = parse_date_param(params[:date])
|
|
8
|
+
@board = TeamDay.new(date: @date)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -2,9 +2,11 @@ module HrLite
|
|
|
2
2
|
module ApplicationHelper
|
|
3
3
|
NAV_ITEMS = [
|
|
4
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" ] },
|
|
5
|
+
{ label: "Attendance", path: :attendance_path, match: [ "/attendance", "/regularization_requests" ] },
|
|
6
|
+
{ label: "Leaves", path: :leave_requests_path, match: [ "/leave_requests", "/leave_balances", "/comp_off_requests" ] },
|
|
7
|
+
{ label: "Team", path: :team_path, match: [ "/team" ] },
|
|
7
8
|
{ label: "Calendar", path: :calendar_path, match: [ "/calendar", "/holidays" ] },
|
|
9
|
+
{ label: "Org", path: :org_path, match: [ "/org" ] },
|
|
8
10
|
{ label: "Kudos", path: :kudos_path, match: [ "/kudos" ] },
|
|
9
11
|
{ label: "Slips", path: :salary_slips_path, match: [ "/salary_slips" ] },
|
|
10
12
|
{ label: "Career", path: :career_path, match: [ "/career", "/appraisals", "/profile" ] }
|
|
@@ -13,7 +15,7 @@ module HrLite
|
|
|
13
15
|
ADMIN_NAV_ITEMS = [
|
|
14
16
|
{ label: "Overview", path: :admin_overview_path, match: [ "/admin/overview" ] },
|
|
15
17
|
{ 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" ] }
|
|
18
|
+
{ label: "Approvals", path: :admin_leave_requests_path, match: [ "/admin/leave_requests", "/admin/leave_balances", "/admin/comp_off_requests", "/admin/regularization_requests" ] }
|
|
17
19
|
].freeze
|
|
18
20
|
|
|
19
21
|
LEADERSHIP_NAV_ITEMS = [
|
|
@@ -85,5 +87,62 @@ module HrLite
|
|
|
85
87
|
def hrl_amount_in_words(amount)
|
|
86
88
|
HrLite::AmountInWords.words(amount)
|
|
87
89
|
end
|
|
90
|
+
|
|
91
|
+
def hrl_duration(seconds)
|
|
92
|
+
return "\u2014" if seconds.nil?
|
|
93
|
+
|
|
94
|
+
total = seconds.to_i
|
|
95
|
+
format("%dh %02dm", total / 3600, (total % 3600) / 60)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Status badge for a TeamDay row — punch times over vague labels.
|
|
99
|
+
def hrl_team_status(row)
|
|
100
|
+
case row.kind
|
|
101
|
+
when :holiday then hrl_status_badge("Holiday", "hrl-badge--muted", worked_hint(row))
|
|
102
|
+
when :weekend then hrl_status_badge("Weekend", "hrl-badge--muted", worked_hint(row))
|
|
103
|
+
when :leave then hrl_status_badge("On leave (#{row.leave.leave_type.code})", "hrl-badge--warn")
|
|
104
|
+
when :half_day_leave then hrl_status_badge("Half-day leave (#{row.leave.leave_type.code})", "hrl-badge--warn", worked_hint(row))
|
|
105
|
+
when :upcoming then hrl_status_badge("\u2014", "hrl-badge--muted")
|
|
106
|
+
when :absent
|
|
107
|
+
label = row.date_today? ? "Not in yet" : "Absent"
|
|
108
|
+
hrl_status_badge(label, "hrl-badge--bad")
|
|
109
|
+
else # :present / :half_day punch
|
|
110
|
+
record = row.record
|
|
111
|
+
if record.check_out_at
|
|
112
|
+
hrl_status_badge("Done #{record.check_in_at.strftime('%H:%M')}\u2013#{record.check_out_at.strftime('%H:%M')}", "hrl-badge--ok")
|
|
113
|
+
elsif row.date_today?
|
|
114
|
+
hrl_status_badge("In since #{record.check_in_at.strftime('%H:%M')}", "hrl-badge--ok")
|
|
115
|
+
else
|
|
116
|
+
hrl_status_badge("In #{record.check_in_at.strftime('%H:%M')}, no check-out", "hrl-badge--warn")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Status pill for a request lifecycle string — one colour map for every
|
|
122
|
+
# list and show screen.
|
|
123
|
+
def hrl_request_status_badge(status)
|
|
124
|
+
css = { "approved" => "hrl-badge--ok", "rejected" => "hrl-badge--bad",
|
|
125
|
+
"cancelled" => "hrl-badge--muted" }[status]
|
|
126
|
+
content_tag(:span, status.humanize, class: "hrl-badge #{css}")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# Always ONE root node — these land inside stacked-table flex cells,
|
|
132
|
+
# where a second sibling would be flung to the far edge.
|
|
133
|
+
def hrl_status_badge(label, css, hint = nil)
|
|
134
|
+
badge = content_tag(:span, label, class: "hrl-badge #{css}")
|
|
135
|
+
return badge unless hint
|
|
136
|
+
|
|
137
|
+
content_tag(:span, safe_join([ badge, " ", content_tag(:span, hint, class: "hrl-small hrl-muted") ]))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# A punch on a holiday/weekend/half-day-leave is worth surfacing —
|
|
141
|
+
# that's exactly what comp-off requests point at.
|
|
142
|
+
def worked_hint(row)
|
|
143
|
+
return nil unless row.record&.check_in_at
|
|
144
|
+
|
|
145
|
+
"worked #{row.record.check_in_at.strftime('%H:%M')}#{row.record.check_out_at ? "\u2013#{row.record.check_out_at.strftime('%H:%M')}" : ""}"
|
|
146
|
+
end
|
|
88
147
|
end
|
|
89
148
|
end
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
module HrLite
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# Leave-year rollover (host schedules it on the leave year's first day —
|
|
3
|
+
# Jan 1 for calendar years, Jul 1 for July–June years): materialize
|
|
4
|
+
# carry-forward into the new year's balance rows. Idempotent — a balance
|
|
5
|
+
# whose carry has already been written is never touched again (manual
|
|
6
|
+
# adjustments stay safe).
|
|
5
7
|
class LeaveYearRolloverJob < ActiveJob::Base
|
|
6
8
|
queue_as :default
|
|
7
9
|
|
|
8
|
-
def perform(year:
|
|
9
|
-
|
|
10
|
+
def perform(year: nil)
|
|
11
|
+
# Resolve "today" in the HR time zone — cron often fires around the
|
|
12
|
+
# boundary midnight, and the host process may run in UTC.
|
|
13
|
+
Time.use_zone(HrLite.config.time_zone) do
|
|
14
|
+
year ||= LeaveYear.current_key
|
|
15
|
+
previous_year = year - 1
|
|
16
|
+
roll(year, previous_year)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
10
21
|
|
|
22
|
+
def roll(year, previous_year)
|
|
11
23
|
LeaveType.active.where(paid: true).where.not(annual_quota: nil).find_each do |type|
|
|
12
24
|
next unless type.carry_forward_cap.positive?
|
|
13
25
|
|
|
@@ -16,7 +28,7 @@ module HrLite
|
|
|
16
28
|
next if balance.persisted? && balance.carried_forward.positive?
|
|
17
29
|
|
|
18
30
|
carry = LeaveBalance.for(user, type, previous_year)
|
|
19
|
-
.available(as_of:
|
|
31
|
+
.available(as_of: LeaveYear.range(previous_year).last)
|
|
20
32
|
carry = [ [ carry, type.carry_forward_cap ].min, 0 ].max
|
|
21
33
|
next if carry.zero? && balance.persisted?
|
|
22
34
|
|