hr_lite 0.5.0 → 0.5.2
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 +157 -1
- data/README.md +312 -99
- data/app/assets/javascripts/hr_lite/confirm.js +31 -0
- data/app/assets/stylesheets/hr_lite/hr_lite.css +28 -0
- data/app/controllers/hr_lite/admin/attendances_controller.rb +16 -1
- data/app/controllers/hr_lite/admin/audit_logs_controller.rb +4 -1
- data/app/controllers/hr_lite/admin/employees_controller.rb +36 -12
- data/app/controllers/hr_lite/admin/leave_balances_controller.rb +15 -8
- data/app/controllers/hr_lite/admin/leave_requests_controller.rb +16 -0
- data/app/controllers/hr_lite/admin/resignations_controller.rb +17 -5
- data/app/controllers/hr_lite/admin/salary_slips_controller.rb +17 -2
- data/app/controllers/hr_lite/admin/superadmin_controller.rb +5 -0
- data/app/controllers/hr_lite/org_controller.rb +6 -1
- data/app/controllers/hr_lite/resignations_controller.rb +6 -1
- data/app/helpers/hr_lite/application_helper.rb +2 -12
- data/app/jobs/hr_lite/application_job.rb +5 -0
- data/app/jobs/hr_lite/daily_digest_job.rb +1 -1
- data/app/jobs/hr_lite/leave_year_rollover_job.rb +1 -1
- data/app/jobs/hr_lite/payroll_auto_draft_job.rb +5 -2
- data/app/models/concerns/hr_lite/audited.rb +11 -4
- data/app/models/hr_lite/appraisal.rb +9 -1
- data/app/models/hr_lite/audit_log.rb +11 -0
- data/app/models/hr_lite/comp_off_request.rb +5 -15
- data/app/models/hr_lite/designation_change.rb +18 -1
- data/app/models/hr_lite/employee_profile.rb +5 -1
- data/app/models/hr_lite/leave_balance.rb +51 -5
- data/app/models/hr_lite/leave_request.rb +9 -1
- data/app/models/hr_lite/leave_type.rb +13 -3
- data/app/models/hr_lite/payroll_run.rb +39 -10
- data/app/models/hr_lite/regularization_request.rb +8 -0
- data/app/models/hr_lite/resignation.rb +27 -0
- data/app/models/hr_lite/salary_slip.rb +7 -3
- data/app/services/hr_lite/attendance_puncher.rb +8 -1
- data/app/services/hr_lite/calculators/esi.rb +7 -4
- data/app/services/hr_lite/calculators/professional_tax.rb +5 -1
- data/app/services/hr_lite/calculators/tds.rb +8 -1
- data/app/services/hr_lite/leave_day_counter.rb +9 -4
- data/app/services/hr_lite/overview_query.rb +12 -1
- data/app/services/hr_lite/payroll_run_processor.rb +5 -0
- data/app/services/hr_lite/slip_builder.rb +54 -8
- data/app/views/hr_lite/admin/attendances/index.html.erb +4 -1
- data/app/views/hr_lite/admin/comp_off_requests/show.html.erb +19 -5
- data/app/views/hr_lite/admin/employees/_form.html.erb +20 -2
- data/app/views/hr_lite/admin/employees/show.html.erb +25 -15
- data/app/views/hr_lite/admin/leave_requests/show.html.erb +37 -5
- data/app/views/hr_lite/admin/overview/index.html.erb +21 -0
- data/app/views/hr_lite/admin/payroll_runs/show.html.erb +3 -1
- data/app/views/hr_lite/admin/regularization_requests/show.html.erb +18 -4
- data/app/views/hr_lite/attendance/_punch_card.html.erb +22 -2
- data/app/views/hr_lite/career/show.html.erb +7 -1
- data/app/views/hr_lite/salary_slips/_slip_detail.html.erb +5 -1
- data/app/views/hr_lite/salary_slips/pdf.html.erb +5 -5
- data/app/views/layouts/hr_lite/application.html.erb +42 -4
- data/config/routes.rb +1 -1
- data/db/migrate/20260807184103_add_live_uniqueness_indexes_to_hr_lite.rb +28 -0
- data/db/migrate/20260807184426_add_fy_opening_to_hr_lite_employee_profiles.rb +14 -0
- data/db/migrate/20260807184939_add_out_of_window_days_to_hr_lite_salary_slips.rb +12 -0
- data/lib/hr_lite/amount_in_words.rb +15 -2
- data/lib/hr_lite/money.rb +18 -0
- data/lib/hr_lite/notifications.rb +23 -8
- data/lib/hr_lite/statutory_rate_card.rb +6 -1
- data/lib/hr_lite/version.rb +1 -1
- data/lib/hr_lite.rb +5 -1
- metadata +5 -1
data/README.md
CHANGED
|
@@ -1,43 +1,66 @@
|
|
|
1
1
|
# hr_lite
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/hr_lite)
|
|
3
4
|
[](https://github.com/kshtzkr/hr_lite/actions/workflows/ci.yml)
|
|
4
|
-
[](https://www.ruby-lang.org)
|
|
6
|
+
[](MIT-LICENSE)
|
|
7
|
+
|
|
8
|
+
A mountable Rails engine that adds a small HRMS to an app you already have:
|
|
9
|
+
attendance with geolocation, leave and holidays, Indian payroll (PF/ESI/PT/TDS)
|
|
10
|
+
with salary-slip PDFs, kudos with @mentions, appraisals and promotions. It
|
|
11
|
+
brings its own models, controllers and views under the `HrLite` namespace and
|
|
12
|
+
talks to your app through configuration hooks — you keep your users and your
|
|
13
|
+
auth, it does the HR.
|
|
14
|
+
|
|
15
|
+
- Mountable engine, isolated namespace (`HrLite`), tables prefixed `hr_lite_`.
|
|
16
|
+
- Bring your own users: any user model and auth (Devise or otherwise) via config hooks.
|
|
17
|
+
- Access tiers: employee (self-service), admin (day-to-day operations),
|
|
18
|
+
leadership (policy and people), and an optional superadmin tier for money.
|
|
19
|
+
- One event bus routes every HR event to in-app bells, employee emails and a
|
|
20
|
+
leadership fan-out, with an append-only audit trail of governing changes.
|
|
21
|
+
- Self-contained UI: mobile-first, dependency-free JS, theming through CSS
|
|
22
|
+
custom properties (`--hrl-*`).
|
|
23
|
+
- PAN/UAN/bank numbers and every money amount use ActiveRecord encryption.
|
|
24
|
+
|
|
25
|
+
## Contents
|
|
26
|
+
|
|
27
|
+
- [Requirements](#requirements)
|
|
28
|
+
- [Installation](#installation)
|
|
29
|
+
- [Quick start (sandbox)](#quick-start-sandbox)
|
|
30
|
+
- [Configuration](#configuration)
|
|
31
|
+
- [Access tiers](#access-tiers)
|
|
32
|
+
- [Features](#features)
|
|
33
|
+
- [Attendance and geolocation](#attendance-and-geolocation)
|
|
34
|
+
- [Leave, comp-off and regularization](#leave-comp-off-and-regularization)
|
|
35
|
+
- [Holidays and calendar](#holidays-and-calendar)
|
|
36
|
+
- [Payroll and salary slips](#payroll-and-salary-slips)
|
|
37
|
+
- [Kudos and @mentions](#kudos-and-mentions)
|
|
38
|
+
- [Notifications and the event bus](#notifications-and-the-event-bus)
|
|
39
|
+
- [Appraisals, promotions and the org chart](#appraisals-promotions-and-the-org-chart)
|
|
40
|
+
- [Recurring jobs](#recurring-jobs)
|
|
41
|
+
- [Rake tasks](#rake-tasks)
|
|
42
|
+
- [Generators](#generators)
|
|
43
|
+
- [Theming](#theming)
|
|
44
|
+
- [Statutory disclaimer](#statutory-disclaimer)
|
|
45
|
+
- [Testing and development](#testing-and-development)
|
|
46
|
+
- [Versioning](#versioning)
|
|
47
|
+
- [Contributing](#contributing)
|
|
48
|
+
- [Documentation](#documentation)
|
|
49
|
+
- [License](#license)
|
|
50
|
+
|
|
51
|
+
## Requirements
|
|
52
|
+
|
|
53
|
+
- Ruby >= 3.2.
|
|
54
|
+
- Rails >= 8.0.
|
|
55
|
+
- Any ActiveRecord database. The migrations use portable column types; the gem
|
|
56
|
+
is developed against PostgreSQL and CI runs on SQLite.
|
|
57
|
+
- ActiveRecord encryption keys configured in the host app — money amounts and
|
|
58
|
+
identity PII (PAN, UAN, bank details) are encrypted at rest. Run
|
|
59
|
+
`bin/rails db:encryption:init` if you have not set keys up yet.
|
|
36
60
|
|
|
37
61
|
## Installation
|
|
38
62
|
|
|
39
|
-
|
|
40
|
-
initializer. Nothing else.
|
|
63
|
+
Add the gem to the host app:
|
|
41
64
|
|
|
42
65
|
```ruby
|
|
43
66
|
# Gemfile
|
|
@@ -45,55 +68,73 @@ gem "hr_lite"
|
|
|
45
68
|
```
|
|
46
69
|
|
|
47
70
|
```bash
|
|
48
|
-
|
|
49
|
-
bin/rails g hr_lite:install
|
|
71
|
+
bundle install
|
|
72
|
+
bin/rails g hr_lite:install # writes the annotated initializer, prints next steps
|
|
73
|
+
bin/rails g hr_lite:install --route # …and mounts the engine at /hr for you
|
|
50
74
|
bin/rails db:migrate # engine migrations load straight from the gem
|
|
51
|
-
bin/rails hr_lite:seed # leave types + fixed national holidays (idempotent)
|
|
75
|
+
bin/rails hr_lite:seed # default leave types + fixed national holidays (idempotent)
|
|
52
76
|
```
|
|
53
77
|
|
|
54
|
-
Migrations
|
|
55
|
-
|
|
56
|
-
|
|
78
|
+
Migrations ship from the gem — `bin/rails db:migrate` picks them up and upgrades
|
|
79
|
+
bring their own migrations, so nothing is copied into your repo and nothing
|
|
80
|
+
drifts. If you prefer copies you can manage, run
|
|
81
|
+
`bin/rails hr_lite:install:migrations` once; the engine detects the copied
|
|
82
|
+
files (`*.hr_lite.rb`) and stops appending its own.
|
|
57
83
|
|
|
58
|
-
|
|
84
|
+
If you did not pass `--route`, mount it yourself:
|
|
59
85
|
|
|
60
86
|
```ruby
|
|
61
|
-
# config/routes.rb
|
|
87
|
+
# config/routes.rb — at a path…
|
|
88
|
+
mount HrLite::Engine => "/hr", as: :hr_lite
|
|
89
|
+
|
|
90
|
+
# …or on a subdomain:
|
|
62
91
|
constraints subdomain: "hr" do
|
|
63
92
|
mount HrLite::Engine => "/", as: :hr_lite
|
|
64
93
|
end
|
|
65
94
|
```
|
|
66
95
|
|
|
67
|
-
|
|
68
|
-
|
|
96
|
+
## Quick start (sandbox)
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
git clone https://github.com/kshtzkr/hr_lite && cd hr_lite
|
|
100
|
+
bin/demo # boots a throwaway app on http://localhost:3999
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The sandbox boots a fresh SQLite database with sample data across three persona
|
|
104
|
+
tiers (leadership, admin, employee — one click to sign in as each) and
|
|
105
|
+
pre-seeded attendance, leaves, kudos, a published payroll run and a shared
|
|
106
|
+
appraisal. The data resets on every restart.
|
|
69
107
|
|
|
70
108
|
## Configuration
|
|
71
109
|
|
|
110
|
+
`bin/rails g hr_lite:install` writes `config/initializers/hr_lite.rb`. Every
|
|
111
|
+
key has a working default, so the engine boots in any app; override what your
|
|
112
|
+
app needs:
|
|
113
|
+
|
|
72
114
|
```ruby
|
|
73
115
|
# config/initializers/hr_lite.rb
|
|
74
116
|
HrLite.configure do |c|
|
|
75
|
-
c.parent_controller = "ApplicationController" #
|
|
117
|
+
c.parent_controller = "ApplicationController" # inherit your auth; resolved once at boot
|
|
76
118
|
c.user_class = "User"
|
|
77
119
|
c.current_user_method = :current_user
|
|
78
120
|
c.authenticate_method = :authenticate_user!
|
|
79
|
-
c.admin_check = ->(user) { user.admin? }
|
|
121
|
+
c.admin_check = ->(user) { user.respond_to?(:admin?) && user.admin? }
|
|
80
122
|
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
# payroll, appraisals. Change the list, not the code.
|
|
123
|
+
# The governing tier: only these people change leave policy, offices,
|
|
124
|
+
# holidays, employee profiles, salary structures, payroll and appraisals.
|
|
84
125
|
c.leadership_emails = ENV.fetch("HR_LEADERSHIP_EMAILS", "").split(",").map(&:strip)
|
|
85
126
|
|
|
86
|
-
# In-app notifications -> your bell system.
|
|
127
|
+
# In-app notifications -> your bell system (optional; a no-op by default).
|
|
87
128
|
c.notify = ->(user:, kind:, title:, body:, path:) {
|
|
88
129
|
StaffNotifier.notify(user, kind: kind, title: title, body: body, path: path)
|
|
89
130
|
}
|
|
90
131
|
|
|
91
|
-
c.mailer_from
|
|
92
|
-
c.
|
|
93
|
-
c.company
|
|
132
|
+
c.mailer_from = "hr@example.com"
|
|
133
|
+
c.public_url_base = "https://hr.example.com" # enables deep links in emails
|
|
134
|
+
c.company = -> { { name: "Acme", address: "Head office address", logo_path: nil } }
|
|
94
135
|
|
|
95
|
-
# Salary-slip PDFs: plug your renderer, or add wicked_pdf to
|
|
96
|
-
#
|
|
136
|
+
# Salary-slip PDFs: plug your renderer, or add wicked_pdf to the bundle and
|
|
137
|
+
# the built-in renderer takes over. With neither, PDF export is disabled.
|
|
97
138
|
c.render_pdf = ->(template:, assigns:, cache_key:) {
|
|
98
139
|
PdfRenderer.render(template: template, assigns: assigns, cache_key: cache_key)
|
|
99
140
|
}
|
|
@@ -103,16 +144,190 @@ HrLite.configure do |c|
|
|
|
103
144
|
end
|
|
104
145
|
```
|
|
105
146
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
147
|
+
`docs/CONFIGURATION.md` documents every key with its type, default and when to
|
|
148
|
+
override it, plus the full event/notification matrix. `parent_controller` is
|
|
149
|
+
resolved once at boot — restart after changing it.
|
|
150
|
+
|
|
151
|
+
## Access tiers
|
|
152
|
+
|
|
153
|
+
Authorization is layered on top of the checks you provide. There is no separate
|
|
154
|
+
role table; a user's tier is derived per request.
|
|
155
|
+
|
|
156
|
+
- **Employee** — every signed-in user. All self-service screens are scoped to
|
|
157
|
+
`current_user` (asking for someone else's record 404s), so a new hire is
|
|
158
|
+
never locked out.
|
|
159
|
+
- **Admin** (`admin_check`, or anyone in leadership) — day-to-day operations:
|
|
160
|
+
team attendance, leave and regularization decisions, the overview board.
|
|
161
|
+
- **Leadership** (`leadership_check` only — admin is not enough) — policy and
|
|
162
|
+
people: leave types, offices, holidays, the weekend rule, employee profiles,
|
|
163
|
+
onboarding/offboarding, the audit trail. Every leadership mutation writes an
|
|
164
|
+
append-only `hr_lite_audit_logs` row and emails leadership with the diff.
|
|
165
|
+
- **Superadmin (money)** (`superadmin_check`) — salary structures, payroll
|
|
166
|
+
runs, salary-slip administration, appraisals and promotions. Set
|
|
167
|
+
`superadmin_emails` to a subset of leadership to separate money from policy.
|
|
168
|
+
Left empty (the default), the money tier is the leadership tier.
|
|
169
|
+
|
|
170
|
+
Leadership is a list you control, not code. The generated initializer reads it
|
|
171
|
+
from `HR_LEADERSHIP_EMAILS` (a comma-separated list) so you can change who
|
|
172
|
+
governs without a deploy — that env var is a convention in the initializer the
|
|
173
|
+
generator writes, not something the gem reads on its own. You can drop the env
|
|
174
|
+
var and assign `c.leadership_emails` directly, or replace `c.leadership_check`
|
|
175
|
+
to derive leadership some other way.
|
|
176
|
+
|
|
177
|
+
`HrLite.admin?(user)`, `HrLite.leadership?(user)` and `HrLite.superadmin?(user)`
|
|
178
|
+
are available if you need the same checks in host code.
|
|
179
|
+
|
|
180
|
+
## Features
|
|
181
|
+
|
|
182
|
+
Once mounted, staff use the portal at the mount point; admins and leadership get
|
|
183
|
+
an `/admin` area. The routes below are relative to the mount path.
|
|
184
|
+
|
|
185
|
+
### Attendance and geolocation
|
|
186
|
+
|
|
187
|
+
Employees check in and out from `/attendance` (`POST check_in` / `check_out`).
|
|
188
|
+
The browser sends latitude, longitude and accuracy with each punch. Geolocation
|
|
189
|
+
never blocks a punch: a punch with no GPS, or one outside every office radius,
|
|
190
|
+
is still recorded and then flagged for review — denying location access must not
|
|
191
|
+
stop anyone from working. Distance is a pure-Ruby haversine, so there is no
|
|
192
|
+
geocoding dependency:
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
HrLite::Geo.distance_m(office.lat, office.lng, punch_lat, punch_lng) # => metres
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Offices are `HrLite::OfficeLocation` records with a `radius_m`; leadership
|
|
199
|
+
manages them under `/admin/office_locations`. `OfficeLocation.covering?(lat,
|
|
200
|
+
lng)` answers whether a point falls inside any active office, and
|
|
201
|
+
`OfficeLocation.nearest(lat, lng)` backs the flag note ("1.2 km from Head
|
|
202
|
+
Office"). Admins see the team's day and flagged punches under
|
|
203
|
+
`/admin/attendances`.
|
|
204
|
+
|
|
205
|
+
### Leave, comp-off and regularization
|
|
206
|
+
|
|
207
|
+
Leave types are policy rows (`HrLite::LeaveType`) with quota, accrual
|
|
208
|
+
(`monthly` or `yearly_upfront`), carry-forward cap and paid/unpaid flag; seeds
|
|
209
|
+
create a sensible default set (CL, SL, EL, LWP, CO). Balances are computed
|
|
210
|
+
against a leave year whose start month is configurable:
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
HrLite.configure { |c| c.leave_year_start_month = 7 } # July–June instead of Jan–Dec
|
|
214
|
+
HrLite::LeaveYear.current_key # => 2026
|
|
215
|
+
HrLite::LeaveYear.label(2026) # => "2026–27"
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Set `leave_year_start_month` once, at install time — balance rows are keyed by
|
|
219
|
+
leave year, and changing it later reinterprets stored balances. Entitlement
|
|
220
|
+
prorates from the joining date (joined on or before the 15th counts that month).
|
|
221
|
+
|
|
222
|
+
Employees apply, cancel and track balances at `/leave_requests` and
|
|
223
|
+
`/leave_balances`; admins approve or reject under `/admin/leave_requests`.
|
|
224
|
+
Comp-off requests (`/comp_off_requests`) credit the comp-off leave type when
|
|
225
|
+
approved; regularization tickets (`/regularization_requests`) let someone who
|
|
226
|
+
forgot to punch propose the real times for admin approval, which writes them
|
|
227
|
+
onto the attendance record with a full audit trail.
|
|
228
|
+
|
|
229
|
+
### Holidays and calendar
|
|
230
|
+
|
|
231
|
+
`bin/rails hr_lite:seed` inserts the three fixed-date national holidays
|
|
232
|
+
(Republic Day, Independence Day, Gandhi Jayanti). Movable festival dates shift
|
|
233
|
+
every year, so leadership adds them under `/admin/holidays` — including a
|
|
234
|
+
bulk-paste flow. Everyone sees the holiday list at `/holidays` and a combined
|
|
235
|
+
company calendar at `/calendar`. The weekend rule (Sunday only, Saturday and
|
|
236
|
+
Sunday, or 2nd and 4th Saturday) is a leadership setting.
|
|
237
|
+
|
|
238
|
+
### Payroll and salary slips
|
|
239
|
+
|
|
240
|
+
Payroll runs one month at a time through
|
|
241
|
+
`draft → processing → review → finalized → published`. A run prorates each
|
|
242
|
+
salary structure by attendance, applies the statutory calculators, and produces
|
|
243
|
+
a salary slip per employee plus a payout-register CSV. Statutory rates live in a
|
|
244
|
+
date-keyed table so a budget change is one new entry and old runs keep computing
|
|
245
|
+
on the card that was in force:
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
HrLite::StatutoryRateCard.for(Date.new(2026, 4, 1)) # => the card effective on/before that month
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Leadership (or the superadmin tier, if set) drives runs under
|
|
252
|
+
`/admin/payroll_runs`; employees see their published slips at `/salary_slips`.
|
|
253
|
+
Salary-slip PDFs render through `config.render_pdf`, or through a built-in
|
|
254
|
+
WickedPdf path if `wicked_pdf` is in the bundle. The exact math, rounding rules
|
|
255
|
+
and what is deliberately not modelled are in `docs/PAYROLL.md` — read it with
|
|
256
|
+
your accountant before the first run.
|
|
257
|
+
|
|
258
|
+
### Kudos and @mentions
|
|
259
|
+
|
|
260
|
+
The kudos wall (`/kudos`) lets staff thank each other with @mentions. The picker
|
|
261
|
+
inserts a plain-text marker, `@[Asha Rao](42)`, and the server parses the ids
|
|
262
|
+
out of it:
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
HrLite::MentionParser.user_ids("Great save @[Asha Rao](42)") # => [42]
|
|
266
|
+
HrLite::MentionParser.strip_markers("Nice @[Asha Rao](42)") # => "Nice @Asha Rao"
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The autocomplete source is `GET <mount>/users/search?q=…`, backed by
|
|
270
|
+
`config.mentionable_users` (a name/email match on your user table by default;
|
|
271
|
+
override it to scope or to use your own search). Each mention notifies the
|
|
272
|
+
mentioned person; the marker is never shown raw in a bell or email.
|
|
273
|
+
|
|
274
|
+
### Notifications and the event bus
|
|
275
|
+
|
|
276
|
+
Domain code publishes an event; a per-event row in the notification matrix
|
|
277
|
+
decides which channels fire — an in-app bell, an employee email, a single
|
|
278
|
+
leadership email with every configured address in `To:`, and a leadership bell:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
HrLite::Notifications.publish(
|
|
282
|
+
"leave.requested",
|
|
283
|
+
title: "Asha requested casual leave",
|
|
284
|
+
bell_to: HrLite.admin_users
|
|
285
|
+
)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The matrix is `HrLite::Notifications::DEFAULT_MATRIX`; override
|
|
289
|
+
`config.notification_matrix` to mute or add channels per event (a host matrix
|
|
290
|
+
pinned on an older gem version still works — unknown events fall back to the
|
|
291
|
+
defaults). Governing changes also publish `policy.changed` with a redacted diff
|
|
292
|
+
and land in the audit trail. The full event list and channel table is in
|
|
293
|
+
`docs/CONFIGURATION.md`.
|
|
109
294
|
|
|
110
|
-
|
|
295
|
+
### Appraisals, promotions and the org chart
|
|
296
|
+
|
|
297
|
+
Appraisals move `draft → shared` and become permanent once shared; employees
|
|
298
|
+
read theirs at `/appraisals`. Promotions and role changes are recorded as
|
|
299
|
+
designation changes with a timeline (`/career`) and can mirror into your own
|
|
300
|
+
user model through `config.on_designation_change`. The org chart at `/org` is
|
|
301
|
+
visible to everyone and shows the reporting tree — names, designations and
|
|
302
|
+
departments only, never salary or private data — with each viewer's own
|
|
303
|
+
reporting line labelled L1/L2/…
|
|
304
|
+
|
|
305
|
+
## Recurring jobs
|
|
306
|
+
|
|
307
|
+
Schedule these on the host's job scheduler (cron, GoodJob, Sidekiq-cron,
|
|
308
|
+
whatever you run). Each is idempotent and sends nothing on a quiet day.
|
|
111
309
|
|
|
112
310
|
| Job | Schedule | Purpose |
|
|
113
311
|
|---|---|---|
|
|
114
|
-
| `HrLite::DailyDigestJob` |
|
|
115
|
-
| `HrLite::
|
|
312
|
+
| `HrLite::DailyDigestJob` | each morning | Leadership digest: who is out today, pending approvals, flagged punches, missing checkouts |
|
|
313
|
+
| `HrLite::PayrollAutoDraftJob` | monthly, on the 1st | Draft and compute the previous month's payroll from attendance, then notify leadership for review (publishing stays a human action) |
|
|
314
|
+
| `HrLite::LeaveYearRolloverJob` | leave year's first day (Jan 1, or Jul 1 for a July–June year) | Materialize carry-forward into the new year's balances |
|
|
315
|
+
|
|
316
|
+
## Rake tasks
|
|
317
|
+
|
|
318
|
+
| Task | What it does |
|
|
319
|
+
|---|---|
|
|
320
|
+
| `hr_lite:seed` | Idempotently seed default leave types and the fixed national holidays. Safe to run on every deploy; it never overwrites operator edits. |
|
|
321
|
+
| `hr_lite:install:migrations` | Copy the engine migrations into the host's `db/migrate` (Rails-provided). Only needed if you want copies instead of gem-served migrations. |
|
|
322
|
+
|
|
323
|
+
## Generators
|
|
324
|
+
|
|
325
|
+
`hr_lite:install` writes `config/initializers/hr_lite.rb` and prints the
|
|
326
|
+
remaining wiring steps.
|
|
327
|
+
|
|
328
|
+
| Option | Default | Effect |
|
|
329
|
+
|---|---|---|
|
|
330
|
+
| `--route` | off | Also append `mount HrLite::Engine => "/hr", as: :hr_lite` to `config/routes.rb`. |
|
|
116
331
|
|
|
117
332
|
## Theming
|
|
118
333
|
|
|
@@ -122,58 +337,56 @@ Override the CSS variables, nothing else:
|
|
|
122
337
|
:root { --hrl-accent: #00a24f; --hrl-font: "Inter", sans-serif; }
|
|
123
338
|
```
|
|
124
339
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
- [docs/CONFIGURATION.md](docs/CONFIGURATION.md) — every config key, the event
|
|
130
|
-
list and the notification matrix, how the three access tiers gate.
|
|
131
|
-
- [docs/PAYROLL.md](docs/PAYROLL.md) — the exact payroll math, rounding rules,
|
|
132
|
-
run lifecycle, and what is deliberately not modelled.
|
|
340
|
+
Point `config.extra_stylesheets` at a stylesheet and it is linked after the
|
|
341
|
+
engine's own CSS, so your `--hrl-*` overrides win. Every view is also
|
|
342
|
+
overridable through standard engine view precedence — drop a file at the same
|
|
343
|
+
path under your app's `app/views/hr_lite/…`.
|
|
133
344
|
|
|
134
345
|
## Statutory disclaimer
|
|
135
346
|
|
|
136
347
|
Payroll math (PF, ESI, PT, TDS) is projection-grade and configured in
|
|
137
|
-
`HrLite::StatutoryRateCard` — a date-keyed rate table.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
348
|
+
`HrLite::StatutoryRateCard` — a date-keyed rate table. Verify the rates for each
|
|
349
|
+
financial year with your accountant before the first run; the per-slip LOP and
|
|
350
|
+
TDS overrides are the escape hatch. Surcharge, perquisites and HRA-exemption
|
|
351
|
+
math are not modelled — see `docs/PAYROLL.md` for the full list of what is left
|
|
352
|
+
to the override.
|
|
141
353
|
|
|
142
|
-
##
|
|
354
|
+
## Testing and development
|
|
143
355
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
- [SemVer](https://semver.org): 0.x minors may break with a CHANGELOG note;
|
|
148
|
-
from 1.0, breaking changes only in majors.
|
|
356
|
+
The test harness is a minimal dummy app in `spec/dummy` — SQLite, a bare `User`
|
|
357
|
+
model and a session-based auth stub — so the suite runs in a few seconds with no
|
|
358
|
+
external services.
|
|
149
359
|
|
|
150
|
-
|
|
360
|
+
```bash
|
|
361
|
+
bundle install
|
|
362
|
+
bundle exec rspec # dummy-app suite
|
|
363
|
+
COVERAGE=1 bundle exec rspec # with SimpleCov (the project holds 100% line coverage)
|
|
364
|
+
bundle exec rubocop # rubocop-rails-omakase style
|
|
365
|
+
```
|
|
151
366
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
367
|
+
## Versioning
|
|
368
|
+
|
|
369
|
+
This project follows [Semantic Versioning](https://semver.org). While on 0.x a
|
|
370
|
+
minor release may include a breaking change, always called out in
|
|
371
|
+
[CHANGELOG.md](CHANGELOG.md); from 1.0 breaking changes land only in majors.
|
|
157
372
|
|
|
158
373
|
## Contributing
|
|
159
374
|
|
|
160
|
-
Bug reports and pull requests are welcome — see
|
|
161
|
-
|
|
162
|
-
coverage, BigDecimal money, config hooks over host couplings
|
|
163
|
-
issues: report privately per [SECURITY.md](SECURITY.md).
|
|
164
|
-
interacting with
|
|
375
|
+
Bug reports and pull requests are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
376
|
+
for setup, the branch and PR flow, and the ground rules (Conventional Commits,
|
|
377
|
+
100% coverage, BigDecimal money, config hooks over host couplings, CI must
|
|
378
|
+
pass). Security issues: report privately per [SECURITY.md](SECURITY.md).
|
|
379
|
+
Everyone interacting with the project is expected to follow the
|
|
165
380
|
[code of conduct](CODE_OF_CONDUCT.md).
|
|
166
381
|
|
|
167
|
-
##
|
|
382
|
+
## Documentation
|
|
168
383
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
```
|
|
384
|
+
- [docs/CONFIGURATION.md](docs/CONFIGURATION.md) — every config key with type and
|
|
385
|
+
default, the event/notification matrix, and how the access tiers gate.
|
|
386
|
+
- [docs/PAYROLL.md](docs/PAYROLL.md) — the exact payroll math, rounding rules,
|
|
387
|
+
run lifecycle, and what is deliberately not modelled.
|
|
388
|
+
- [CHANGELOG.md](CHANGELOG.md) — release history.
|
|
175
389
|
|
|
176
390
|
## License
|
|
177
391
|
|
|
178
|
-
|
|
179
|
-
[MIT License](MIT-LICENSE).
|
|
392
|
+
hr_lite is released under the terms of the [MIT License](MIT-LICENSE).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Confirmation prompts for destructive actions.
|
|
2
|
+
//
|
|
3
|
+
// The views carry `data-turbo-confirm` on the forms that delete a draft
|
|
4
|
+
// payroll run, remove a kudos, offboard someone or withdraw a resignation —
|
|
5
|
+
// but the engine deliberately ships no Turbo and no Stimulus, so nothing was
|
|
6
|
+
// ever reading that attribute. Every one of those buttons fired on the first
|
|
7
|
+
// click with no prompt at all.
|
|
8
|
+
//
|
|
9
|
+
// Same attribute name so the markup does not change, and a no-op when a host
|
|
10
|
+
// app does load Turbo (Turbo handles it first; two prompts would be worse
|
|
11
|
+
// than none).
|
|
12
|
+
(function () {
|
|
13
|
+
if (window.Turbo) return;
|
|
14
|
+
|
|
15
|
+
document.addEventListener(
|
|
16
|
+
"submit",
|
|
17
|
+
function (event) {
|
|
18
|
+
var form = event.target;
|
|
19
|
+
if (!form || form.nodeName !== "FORM") return;
|
|
20
|
+
|
|
21
|
+
var message = form.getAttribute("data-turbo-confirm");
|
|
22
|
+
if (!message) return;
|
|
23
|
+
|
|
24
|
+
if (!window.confirm(message)) {
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
event.stopPropagation();
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
true // capture, so the check runs before any other submit handler
|
|
30
|
+
);
|
|
31
|
+
})();
|
|
@@ -75,6 +75,34 @@
|
|
|
75
75
|
padding: .25rem .25rem calc(.25rem + env(safe-area-inset-bottom));
|
|
76
76
|
}
|
|
77
77
|
.hrl-tabbar .hrl-nav__link { flex: 1; justify-content: center; font-size: .78rem; }
|
|
78
|
+
/* "More" sheet — everything five tabs cannot hold. Pure <details>: the
|
|
79
|
+
engine ships no Turbo or Stimulus and this must not need any. */
|
|
80
|
+
.hrl-tabbar__more { flex: 1; display: flex; }
|
|
81
|
+
.hrl-tabbar__more > summary {
|
|
82
|
+
flex: 1; justify-content: center; list-style: none; cursor: pointer;
|
|
83
|
+
}
|
|
84
|
+
.hrl-tabbar__more > summary::-webkit-details-marker { display: none; }
|
|
85
|
+
.hrl-tabbar__more[open] > summary { color: var(--hrl-accent); font-weight: 600; }
|
|
86
|
+
.hrl-sheet {
|
|
87
|
+
position: fixed; left: 0; right: 0;
|
|
88
|
+
bottom: calc(3.3rem + env(safe-area-inset-bottom));
|
|
89
|
+
max-height: 60vh; overflow-y: auto;
|
|
90
|
+
display: flex; flex-direction: column; gap: 2px;
|
|
91
|
+
background: var(--hrl-card); border-top: 1px solid var(--hrl-line);
|
|
92
|
+
padding: .6rem .85rem;
|
|
93
|
+
box-shadow: 0 -6px 18px rgb(0 0 0 / 8%);
|
|
94
|
+
}
|
|
95
|
+
/* Left-aligned like the desktop rail — the tab-bar rule centres its own
|
|
96
|
+
links and would otherwise apply here too. */
|
|
97
|
+
.hrl-sheet .hrl-nav__link {
|
|
98
|
+
display: flex; justify-content: flex-start;
|
|
99
|
+
min-height: 44px; font-size: .92rem; padding: .5rem .6rem;
|
|
100
|
+
}
|
|
101
|
+
.hrl-sheet .hrl-side__group {
|
|
102
|
+
color: var(--hrl-muted); font-size: .72rem; font-weight: 700;
|
|
103
|
+
text-transform: uppercase; letter-spacing: .06em;
|
|
104
|
+
margin: .7rem 0 .15rem; padding: 0 .6rem;
|
|
105
|
+
}
|
|
78
106
|
|
|
79
107
|
@media (min-width: 768px) {
|
|
80
108
|
.hrl-tabbar { display: none; }
|
|
@@ -35,12 +35,27 @@ module HrLite
|
|
|
35
35
|
|
|
36
36
|
attrs = params.require(:attendance_record).permit(:check_in_at, :check_out_at, :status)
|
|
37
37
|
if attrs[:check_in_at].blank? && attrs[:check_out_at].blank?
|
|
38
|
-
|
|
38
|
+
# Nothing to remove used to still write a `destroy` audit row with
|
|
39
|
+
# subject_id 0 and email the employee that a punch was removed.
|
|
40
|
+
unless record.persisted?
|
|
41
|
+
return redirect_to admin_attendance_path(@employee.id, month: date.strftime("%Y-%m")),
|
|
42
|
+
notice: "Nothing to remove for that day."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
record.destroy
|
|
39
46
|
log_regularization(record, note, removed: true)
|
|
40
47
|
return redirect_to admin_attendance_path(@employee.id, month: date.strftime("%Y-%m")),
|
|
41
48
|
notice: "Punch removed."
|
|
42
49
|
end
|
|
43
50
|
|
|
51
|
+
# A check-out with no check-in produces a row every consumer reads as
|
|
52
|
+
# absent (they all key off check_in_at), so the day silently stays LOP
|
|
53
|
+
# while the screen reports the fix succeeded.
|
|
54
|
+
if attrs[:check_in_at].blank?
|
|
55
|
+
return redirect_to admin_attendance_path(@employee.id, date: date, month: date.strftime("%Y-%m")),
|
|
56
|
+
alert: "A check-in time is required when a check-out is set."
|
|
57
|
+
end
|
|
58
|
+
|
|
44
59
|
record.assign_attributes(attrs)
|
|
45
60
|
record.status = "present" if record.status.blank?
|
|
46
61
|
record.regularized_by_id = hr_current_user.id
|
|
@@ -2,7 +2,10 @@ module HrLite
|
|
|
2
2
|
module Admin
|
|
3
3
|
class AuditLogsController < LeadershipController
|
|
4
4
|
def index
|
|
5
|
-
|
|
5
|
+
# Appraisal and promotion rows carry review text in plain columns:
|
|
6
|
+
# ordinary leadership governs people and policy, not pay.
|
|
7
|
+
scope = hr_superadmin? ? AuditLog.recent : AuditLog.recent.outside_money_tier
|
|
8
|
+
@audit_logs = paginate(scope.includes(:actor))
|
|
6
9
|
end
|
|
7
10
|
end
|
|
8
11
|
end
|