hr_lite 0.15.0 → 0.16.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 +48 -0
- data/README.md +4 -1
- data/app/assets/javascripts/hr_lite/print.js +5 -0
- data/app/assets/stylesheets/hr_lite/hr_lite.css +72 -0
- data/app/controllers/hr_lite/admin/employees_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/holidays_controller.rb +7 -0
- data/app/controllers/hr_lite/admin/leave_requests_controller.rb +23 -0
- data/app/controllers/hr_lite/admin/policies_controller.rb +29 -0
- data/app/controllers/hr_lite/hr_requests_controller.rb +2 -0
- data/app/controllers/hr_lite/id_cards_controller.rb +25 -0
- data/app/helpers/hr_lite/application_helper.rb +9 -0
- data/app/models/hr_lite/employee_profile.rb +21 -3
- data/app/models/hr_lite/hr_request.rb +12 -3
- data/app/models/hr_lite/leave_request.rb +15 -4
- data/app/models/hr_lite/policy.rb +15 -0
- data/app/models/hr_lite/setting.rb +1 -1
- data/app/views/hr_lite/admin/employees/_form.html.erb +5 -0
- data/app/views/hr_lite/admin/employees/show.html.erb +1 -0
- data/app/views/hr_lite/admin/hr_requests/show.html.erb +3 -0
- data/app/views/hr_lite/admin/leave_requests/index.html.erb +3 -0
- data/app/views/hr_lite/admin/leave_requests/new.html.erb +35 -0
- data/app/views/hr_lite/admin/policies/index.html.erb +27 -0
- data/app/views/hr_lite/admin/policies/new.html.erb +22 -0
- data/app/views/hr_lite/id_cards/show.html.erb +86 -0
- data/app/views/hr_lite/shared/_bell.html.erb +3 -0
- data/app/views/layouts/hr_lite/application.html.erb +2 -0
- data/config/routes.rb +4 -1
- data/db/migrate/20260927100145_add_id_card_fields_to_hr_lite_employee_profiles.rb +6 -0
- data/db/migrate/20260927101031_add_created_by_id_to_hr_lite_leave_requests.rb +5 -0
- data/lib/hr_lite/configuration.rb +4 -1
- data/lib/hr_lite/notifications.rb +4 -3
- data/lib/hr_lite/version.rb +1 -1
- data/lib/hr_lite.rb +1 -0
- metadata +25 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 14d37a1d62a035315390be2c619a74722c58e6b53b747422aa4ab48458287102
|
|
4
|
+
data.tar.gz: fbbb6c7efb893ff38d6992971844fd721deca4819943f0e0dd8f1cd343f4b4fc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 70b42e3a35bd683ab62ecda90d33009511faeda8595cbc9ec9fc9f1ae2388f61592059a4ea1be151ade6ec3062f4ecea2726098f2f58adc55048f8e108de7460
|
|
7
|
+
data.tar.gz: a19199e7cd8a173713e97d77266d4779951ef47d70171c234ee65d2afec5cc35be8670542db4e49ad2e656efe6031b8aba053cc281f687fd08dd6b20e3d83511
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.16.0] - 2026-09-27
|
|
11
|
+
|
|
12
|
+
Employee ID cards, print requests through Ask HR, recording leave for
|
|
13
|
+
somebody, and notices that reach the right people. Two migrations, both
|
|
14
|
+
additive (`blood_group`/`emergency_contact` on profiles, `created_by_id` on
|
|
15
|
+
leave requests). New runtime dependency: `rqrcode`.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Employee ID card.** `/id_card` shows the card front and back; `/id_card/:user_id`
|
|
20
|
+
lets anyone whose `profile.view` reaches that person open it to print. Print
|
|
21
|
+
gives one CR80 (54 × 85.6 mm) face per page. The QR holds the company brand,
|
|
22
|
+
address and phone as plain text — no URL, nothing to host.
|
|
23
|
+
- **ID photo.** The one field an employee edits on their own profile. JPG, PNG
|
|
24
|
+
or WebP under 5 MB, content-sniffed; every change is audited.
|
|
25
|
+
- Blood group and emergency contact on the leadership employee form, encrypted
|
|
26
|
+
like the other personal fields so the audit email reads `[changed]`.
|
|
27
|
+
- `config.company` may return `brand:` and `phone:`.
|
|
28
|
+
- **Request a printed ID card.** One tap on the card raises an Ask-HR request
|
|
29
|
+
in the new `id_card` category. It needs a photo first, and only one can be
|
|
30
|
+
open at a time. The desk opens the card from the request to print it;
|
|
31
|
+
answering the request stamps "Issued" on the card.
|
|
32
|
+
- **Record leave for somebody.** Approvals → Record leave, for leave taken but
|
|
33
|
+
never applied for. It is saved approved by whoever records it, keeps the
|
|
34
|
+
balance check, stores `created_by_id`, and emails the employee — not the
|
|
35
|
+
approvers. The employee is picked by typing a name or code.
|
|
36
|
+
- A new hire gets the Employee role with their profile, so Ask HR and
|
|
37
|
+
Policies work from day one.
|
|
38
|
+
- **Publish policies and announcements.** Leadership (`policy.manage`) writes
|
|
39
|
+
one at `/admin/policies`; every current employee gets an email and a bell.
|
|
40
|
+
A policy asks to be acknowledged, an announcement does not, and reusing a
|
|
41
|
+
title publishes the next version. The list shows who still has to
|
|
42
|
+
acknowledge.
|
|
43
|
+
- **Holiday notices.** Adding a holiday tells everyone; a pasted list sends one
|
|
44
|
+
notice, not one per line (new matrix row `holiday.published`).
|
|
45
|
+
- `config.notifications = ->(user) { { url:, unread: } }` puts a notifications
|
|
46
|
+
link with the unread count in the HR shell. Off by default.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- Notification defaults: a leave application and an Ask-HR request now EMAIL
|
|
51
|
+
every approver / desk holder as well as belling them; the team "on leave"
|
|
52
|
+
notice is a bell only, and is not sent at all for leave already over.
|
|
53
|
+
- Overlap and attendance errors on a leave request no longer say "you" — HR
|
|
54
|
+
sees them too now.
|
|
55
|
+
- Employee codes are zero-padded to six digits, and the prefix may end in one
|
|
56
|
+
hyphen — `ESA-` gives `ESA-000001`. Existing codes are left as they are.
|
|
57
|
+
|
|
10
58
|
## [0.15.0] - 2026-08-18
|
|
11
59
|
|
|
12
60
|
The screens for three features that shipped as tables nobody could reach. No
|
data/README.md
CHANGED
|
@@ -131,7 +131,10 @@ HrLite.configure do |c|
|
|
|
131
131
|
|
|
132
132
|
c.mailer_from = "hr@example.com"
|
|
133
133
|
c.public_url_base = "https://hr.example.com" # enables deep links in emails
|
|
134
|
-
|
|
134
|
+
# brand + phone are optional; the ID card prints brand, and its QR holds
|
|
135
|
+
# brand, address and phone as plain text.
|
|
136
|
+
c.company = -> { { name: "Acme", brand: "acme.in", address: "Head office address",
|
|
137
|
+
phone: "+91 00000 00000", logo_url: nil } }
|
|
135
138
|
|
|
136
139
|
# Salary-slip PDFs: plug your renderer, or add wicked_pdf to the bundle and
|
|
137
140
|
# the built-in renderer takes over. With neither, PDF export is disabled.
|
|
@@ -324,3 +324,75 @@
|
|
|
324
324
|
.hrl-prose { max-width: 68ch; }
|
|
325
325
|
.hrl-prose p { margin: 0 0 .8rem; line-height: 1.6; }
|
|
326
326
|
.hrl-prose p:last-child { margin-bottom: 0; }
|
|
327
|
+
|
|
328
|
+
/* ID card. Every length is N × --u, where N is the design's pixel value at
|
|
329
|
+
360px wide — so one variable fits a phone, the desktop, and a CR80 print. */
|
|
330
|
+
.hrl-idcard-faces { container-type: inline-size; display: flex; flex-wrap: wrap; gap: 3.5rem; justify-content: center; }
|
|
331
|
+
.hrl-idcard-face { display: flex; flex-direction: column; align-items: center; gap: .85rem; }
|
|
332
|
+
.hrl-idcard__label { font: 600 11px/1 var(--hrl-card-body, var(--hrl-font)); letter-spacing: .14em; color: #64748B; }
|
|
333
|
+
.hrl-idcard {
|
|
334
|
+
--u: min(1px, 100cqi / 360);
|
|
335
|
+
--blue: #0057A8; --green: #00A24F; --red: #ED1C24;
|
|
336
|
+
width: calc(360 * var(--u)); height: calc(570 * var(--u));
|
|
337
|
+
background: #fff; border: calc(1.4 * var(--u)) solid #D9D9D9; border-radius: calc(22 * var(--u));
|
|
338
|
+
box-shadow: 0 6px 18px rgba(15, 23, 42, .08), 0 24px 48px rgba(15, 23, 42, .08);
|
|
339
|
+
overflow: hidden; position: relative; display: flex; flex-direction: column;
|
|
340
|
+
font-family: var(--hrl-card-body, var(--hrl-font)); color: #0F172A; print-color-adjust: exact; -webkit-print-color-adjust: exact;
|
|
341
|
+
}
|
|
342
|
+
.hrl-idcard--back { align-items: center; }
|
|
343
|
+
.hrl-idcard__band { height: calc(176 * var(--u)); background: var(--blue); display: flex; flex-direction: column; align-items: center; padding-top: calc(18 * var(--u)); }
|
|
344
|
+
.hrl-idcard__slot { width: calc(64 * var(--u)); height: calc(10 * var(--u)); border-radius: 9999px; background: #F9F9F9; box-shadow: inset 0 1px 3px rgba(0, 0, 0, .25); }
|
|
345
|
+
.hrl-idcard__slot--back { margin-top: calc(18 * var(--u)); background: #F1F5F9; box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2); }
|
|
346
|
+
.hrl-idcard__logo { margin-top: calc(18 * var(--u)); background: #fff; border-radius: 9999px; padding: calc(8 * var(--u)) calc(18 * var(--u)); display: flex; align-items: center; font: 700 calc(13 * var(--u)) var(--hrl-card-display, var(--hrl-font)); color: var(--blue); }
|
|
347
|
+
.hrl-idcard__logo img { height: calc(26 * var(--u)); display: block; }
|
|
348
|
+
.hrl-idcard__kicker { margin-top: calc(12 * var(--u)); font: 600 calc(10 * var(--u))/1 var(--hrl-card-body, var(--hrl-font)); letter-spacing: .2em; color: rgba(255, 255, 255, .85); }
|
|
349
|
+
.hrl-idcard__photo {
|
|
350
|
+
position: absolute; top: calc(126 * var(--u)); left: 50%; transform: translateX(-50%);
|
|
351
|
+
width: calc(140 * var(--u)); height: calc(164 * var(--u)); border-radius: calc(26 * var(--u));
|
|
352
|
+
border: calc(5 * var(--u)) solid #fff; box-shadow: 0 6px 18px rgba(15, 23, 42, .15); background: #F8FAFC; overflow: hidden;
|
|
353
|
+
display: flex; align-items: center; justify-content: center; font: 700 calc(34 * var(--u)) var(--hrl-card-display, var(--hrl-font)); color: #64748B;
|
|
354
|
+
}
|
|
355
|
+
.hrl-idcard__photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
356
|
+
.hrl-idcard__who { margin-top: calc(124 * var(--u)); text-align: center; padding: 0 calc(24 * var(--u)); }
|
|
357
|
+
.hrl-idcard__name { font: 800 calc(25 * var(--u))/1.1 var(--hrl-card-display, var(--hrl-font)); letter-spacing: -.02em; text-wrap: balance; }
|
|
358
|
+
.hrl-idcard__role { margin-top: calc(6 * var(--u)); font: 500 calc(13 * var(--u))/1.3 var(--hrl-card-body, var(--hrl-font)); color: #334155; }
|
|
359
|
+
.hrl-idcard__green { color: var(--green); }
|
|
360
|
+
.hrl-idcard__blue { color: var(--blue); }
|
|
361
|
+
.hrl-idcard__red { color: var(--red) !important; }
|
|
362
|
+
.hrl-idcard__facts {
|
|
363
|
+
margin: calc(18 * var(--u)) calc(24 * var(--u)) 0; display: grid; grid-template-columns: 1fr 1fr; gap: calc(12 * var(--u)) calc(16 * var(--u));
|
|
364
|
+
padding: calc(14 * var(--u)) calc(16 * var(--u)); background: #F8FAFC; border-radius: calc(16 * var(--u)); border: 1px solid #E2E8F0;
|
|
365
|
+
}
|
|
366
|
+
.hrl-idcard__facts dt { font: 600 calc(9 * var(--u))/1 var(--hrl-card-body, var(--hrl-font)); letter-spacing: .14em; color: #64748B; }
|
|
367
|
+
.hrl-idcard__facts dd { margin: calc(5 * var(--u)) 0 0; font: 600 calc(13 * var(--u))/1.2 var(--hrl-card-body, var(--hrl-font)); color: #0F172A; }
|
|
368
|
+
.hrl-idcard__strip { margin-top: auto; height: calc(10 * var(--u)); display: grid; grid-template-columns: 2fr 1fr 1fr; }
|
|
369
|
+
.hrl-idcard__strip span:nth-child(1) { background: var(--blue); }
|
|
370
|
+
.hrl-idcard__strip span:nth-child(2) { background: var(--green); }
|
|
371
|
+
.hrl-idcard__strip span:nth-child(3) { background: var(--red); }
|
|
372
|
+
.hrl-idcard__title { margin-top: calc(26 * var(--u)); font: 800 calc(18 * var(--u))/1.1 var(--hrl-card-display, var(--hrl-font)); letter-spacing: -.01em; }
|
|
373
|
+
.hrl-idcard__qr { margin-top: calc(16 * var(--u)); padding: calc(14 * var(--u)); border-radius: calc(24 * var(--u)); border: calc(1.4 * var(--u)) solid #D9D9D9; box-shadow: 0 6px 18px rgba(15, 23, 42, .08); }
|
|
374
|
+
.hrl-idcard__qr svg { width: calc(176 * var(--u)); height: calc(176 * var(--u)); display: block; }
|
|
375
|
+
.hrl-idcard__code { margin-top: calc(12 * var(--u)); font: 600 calc(12 * var(--u))/1 var(--hrl-card-body, var(--hrl-font)); letter-spacing: .16em; }
|
|
376
|
+
.hrl-idcard__rows { margin: calc(22 * var(--u)) calc(28 * var(--u)) 0; align-self: stretch; display: flex; flex-direction: column; gap: calc(10 * var(--u)); }
|
|
377
|
+
.hrl-idcard__rows div { display: flex; justify-content: space-between; gap: calc(12 * var(--u)); font: 400 calc(12 * var(--u))/1.4 var(--hrl-card-body, var(--hrl-font)); color: #475569; }
|
|
378
|
+
.hrl-idcard__rows b { font-weight: 600; color: #0F172A; text-align: right; }
|
|
379
|
+
.hrl-idcard__rows hr { margin: 0; border: 0; height: 1px; background: #E2E8F0; }
|
|
380
|
+
.hrl-idcard__note { margin: calc(20 * var(--u)) calc(28 * var(--u)) 0; text-align: center; font: 400 calc(11 * var(--u))/1.5 var(--hrl-card-body, var(--hrl-font)); color: #64748B; text-wrap: pretty; }
|
|
381
|
+
.hrl-idcard__foot { margin-top: auto; align-self: stretch; background: var(--blue); padding: calc(14 * var(--u)) calc(24 * var(--u)); display: flex; justify-content: space-between; align-items: center; }
|
|
382
|
+
.hrl-idcard__foot b { font: 700 calc(13 * var(--u))/1 var(--hrl-card-display, var(--hrl-font)); color: #fff; }
|
|
383
|
+
.hrl-idcard__foot span { font: 500 calc(10 * var(--u))/1 var(--hrl-card-body, var(--hrl-font)); letter-spacing: .14em; color: rgba(255, 255, 255, .85); }
|
|
384
|
+
.hrl-idcard-actions { display: flex; flex-wrap: wrap; gap: .85rem; align-items: flex-start; justify-content: center; margin-top: 1.5rem; }
|
|
385
|
+
|
|
386
|
+
/* Print: one CR80 face per page, nothing else. */
|
|
387
|
+
@media print {
|
|
388
|
+
@page { size: 54mm 85.6mm; margin: 0; }
|
|
389
|
+
.hrl-side, .hrl-appbar, .hrl-tabbar, .hrl-flash, .hrl-page__head, .hrl-idcard__label, .hrl-idcard-actions { display: none !important; }
|
|
390
|
+
.hrl-body { background: #fff; }
|
|
391
|
+
.hrl-main { padding: 0; margin: 0; }
|
|
392
|
+
.hrl-idcard-faces { display: block; }
|
|
393
|
+
.hrl-idcard { --u: calc(54mm / 360); box-shadow: none; break-after: page; }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* Notifications link, shown only when the host configures one. */
|
|
397
|
+
.hrl-bell { display: inline-flex; align-items: center; gap: .4rem; min-height: 44px; font-size: .86rem; font-weight: 600; }
|
|
398
|
+
.hrl-bell__count { min-width: 1.4rem; padding: 0 .4rem; border-radius: 9999px; background: var(--hrl-accent); color: var(--hrl-accent-ink); font-size: .75rem; text-align: center; }
|
|
@@ -115,7 +115,7 @@ module HrLite
|
|
|
115
115
|
:user_id, :manager_id, :designation, :date_of_birth, :date_of_joining,
|
|
116
116
|
:date_of_exit, :department, :work_location, :pan_number, :pf_uan, :esi_number,
|
|
117
117
|
:bank_account_number, :bank_ifsc, :bank_name, :tax_regime, :declared_annual_deductions,
|
|
118
|
-
:fy_opening_gross, :fy_opening_tds
|
|
118
|
+
:fy_opening_gross, :fy_opening_tds, :blood_group, :emergency_contact
|
|
119
119
|
)
|
|
120
120
|
end
|
|
121
121
|
|
|
@@ -11,6 +11,7 @@ module HrLite
|
|
|
11
11
|
def create
|
|
12
12
|
@holiday = Holiday.new(holiday_params)
|
|
13
13
|
if @holiday.save
|
|
14
|
+
announce("Holiday added: #{@holiday.name} on #{@holiday.date.strftime('%a %d %b %Y')}")
|
|
14
15
|
redirect_to admin_holidays_path(year: @holiday.date.year), notice: "Holiday added."
|
|
15
16
|
else
|
|
16
17
|
redirect_to admin_holidays_path, alert: @holiday.errors.full_messages.to_sentence
|
|
@@ -55,6 +56,7 @@ module HrLite
|
|
|
55
56
|
created += 1
|
|
56
57
|
end
|
|
57
58
|
|
|
59
|
+
announce("#{created} holiday#{'s' unless created == 1} added to the calendar") if created.positive?
|
|
58
60
|
message = "#{created} holiday#{'s' unless created == 1} added."
|
|
59
61
|
message += " Problems: #{problems.join('; ')}" if problems.any?
|
|
60
62
|
redirect_to admin_holidays_path, problems.any? ? { alert: message } : { notice: message }
|
|
@@ -62,6 +64,11 @@ module HrLite
|
|
|
62
64
|
|
|
63
65
|
private
|
|
64
66
|
|
|
67
|
+
def announce(title)
|
|
68
|
+
staff = HrLite.active_employees
|
|
69
|
+
Notifications.publish("holiday.published", title: title, path: "/holidays", bell_to: staff, email_to: staff)
|
|
70
|
+
end
|
|
71
|
+
|
|
65
72
|
def holiday_params
|
|
66
73
|
params.require(:holiday).permit(:date, :name, :optional)
|
|
67
74
|
end
|
|
@@ -16,6 +16,27 @@ module HrLite
|
|
|
16
16
|
@balance = @request.balance
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# HR records leave somebody forgot to apply for. Approved on save, by the
|
|
20
|
+
# person recording it; the balance check still applies.
|
|
21
|
+
def new
|
|
22
|
+
@request = LeaveRequest.new(start_date: Date.current, end_date: Date.current)
|
|
23
|
+
@people = recordable
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def create
|
|
27
|
+
profile = recordable.find_by(employee_code: params[:employee_code].to_s.strip)
|
|
28
|
+
@request = LeaveRequest.new(params.require(:leave_request).permit(:leave_type_id, :start_date, :end_date, :half_day, :reason)
|
|
29
|
+
.merge(user_id: profile&.user_id, created_by_id: hr_current_user.id))
|
|
30
|
+
recorded = profile && LeaveRequest.transaction do
|
|
31
|
+
(@request.save && @request.approve!(actor: hr_current_user, note: "Recorded by HR")) || raise(ActiveRecord::Rollback)
|
|
32
|
+
end
|
|
33
|
+
return redirect_to(admin_leave_request_path(@request), notice: "Leave recorded and approved.") if recorded
|
|
34
|
+
|
|
35
|
+
@request.errors.add(:base, profile ? "Not enough balance — adjust it first or use unpaid leave" : "Pick an employee from the list") if @request.errors.empty?
|
|
36
|
+
@people = recordable
|
|
37
|
+
render :new, status: :unprocessable_entity
|
|
38
|
+
end
|
|
39
|
+
|
|
19
40
|
def approve
|
|
20
41
|
request = find_decidable
|
|
21
42
|
if request.approve!(actor: hr_current_user, note: params[:decision_note].presence)
|
|
@@ -68,6 +89,8 @@ module HrLite
|
|
|
68
89
|
end
|
|
69
90
|
|
|
70
91
|
def find_decidable = decidable.find(params[:id])
|
|
92
|
+
|
|
93
|
+
def recordable = hr_scope(EmployeeProfile.includes(:user), "leave.approve")
|
|
71
94
|
end
|
|
72
95
|
end
|
|
73
96
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
# Write a policy or an announcement and send it to everyone. A policy asks
|
|
4
|
+
# to be acknowledged; an announcement does not. Same title again = the
|
|
5
|
+
# next version.
|
|
6
|
+
class PoliciesController < ApplicationController
|
|
7
|
+
before_action { hr_require_permission!("policy.manage", scope: :all) }
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
@policies = Policy.newest_first
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def new
|
|
14
|
+
@policy = Policy.new(effective_from: Date.current, acknowledgement_required: true)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create
|
|
18
|
+
@policy = Policy.new(params.require(:policy).permit(:title, :body, :effective_from, :acknowledgement_required)
|
|
19
|
+
.merge(published: true))
|
|
20
|
+
@policy.version = (Policy.where(title: @policy.title).maximum(:version) || 0) + 1
|
|
21
|
+
if @policy.save
|
|
22
|
+
redirect_to admin_policies_path, notice: "Published — everyone has been told."
|
|
23
|
+
else
|
|
24
|
+
render :new, status: :unprocessable_entity
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -19,6 +19,8 @@ module HrLite
|
|
|
19
19
|
@request = HrRequest.new(request_params.merge(user_id: hr_current_user.id))
|
|
20
20
|
if @request.save
|
|
21
21
|
redirect_to hr_requests_path, notice: "Sent to HR."
|
|
22
|
+
elsif @request.category == "id_card"
|
|
23
|
+
redirect_to id_card_path, alert: @request.errors.full_messages.to_sentence
|
|
22
24
|
else
|
|
23
25
|
render :new, status: :unprocessable_entity
|
|
24
26
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# The employee ID card: your own, or — for HR printing it — anyone your
|
|
3
|
+
# `profile.view` reaches. The photo is the one thing an employee edits.
|
|
4
|
+
class IdCardsController < ApplicationController
|
|
5
|
+
def show
|
|
6
|
+
@user = params[:user_id] ? HrLite.user_klass.find(params[:user_id]) : hr_current_user
|
|
7
|
+
hr_require_reach!("profile.view", @user) unless @user == hr_current_user
|
|
8
|
+
@profile = EmployeeProfile.find_by(user_id: @user.id)
|
|
9
|
+
card_requests = HrRequest.where(user_id: @user.id, category: "id_card")
|
|
10
|
+
@issued_on = card_requests.where(status: "resolved").maximum(:resolved_at)
|
|
11
|
+
@print_requested = card_requests.open_requests.pick(:created_at)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def photo
|
|
15
|
+
profile = EmployeeProfile.find_by!(user_id: hr_current_user.id)
|
|
16
|
+
profile.photo = params.require(:photo)
|
|
17
|
+
if profile.save
|
|
18
|
+
AuditLog.record!(action: "id_card.photo_changed", subject: profile, actor: hr_current_user)
|
|
19
|
+
redirect_to id_card_path, notice: "Photo updated.", status: :see_other
|
|
20
|
+
else
|
|
21
|
+
redirect_to id_card_path, alert: profile.errors.full_messages.to_sentence, status: :see_other
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -21,6 +21,7 @@ module HrLite
|
|
|
21
21
|
{ label: "Slips", path: :salary_slips_path, match: [ "/salary_slips" ] },
|
|
22
22
|
{ label: "Career", path: :career_path, match: [ "/career", "/appraisals", "/profile" ] },
|
|
23
23
|
{ label: "Documents", path: :documents_path, match: [ "/documents" ] },
|
|
24
|
+
{ label: "ID card", path: :id_card_path, match: [ "/id_card" ] },
|
|
24
25
|
{ label: "Tax", path: :tax_declaration_path, match: [ "/tax_declaration" ] },
|
|
25
26
|
{ label: "Loans", path: :loans_path, match: [ "/loans" ] }
|
|
26
27
|
].freeze
|
|
@@ -40,6 +41,7 @@ module HrLite
|
|
|
40
41
|
{ label: "Employees", path: :admin_employees_path, match: [ "/admin/employees" ] },
|
|
41
42
|
{ label: "Settings", path: :admin_leave_types_path, match: [ "/admin/leave_types", "/admin/office_locations", "/admin/holidays", "/admin/setting" ] },
|
|
42
43
|
{ label: "Audit", path: :admin_audit_logs_path, match: [ "/admin/audit_logs" ] },
|
|
44
|
+
{ label: "Policies", path: :admin_policies_path, match: [ "/admin/policies" ] },
|
|
43
45
|
{ label: "Documents", path: :admin_documents_path, match: [ "/admin/documents" ] }
|
|
44
46
|
].freeze
|
|
45
47
|
|
|
@@ -54,6 +56,13 @@ module HrLite
|
|
|
54
56
|
{ label: "Loans", path: :admin_loans_path, match: [ "/admin/loans" ] }
|
|
55
57
|
].freeze
|
|
56
58
|
|
|
59
|
+
# The ID card's QR: office address and phone as plain text, so any phone
|
|
60
|
+
# camera shows who to call about a found card. No URL, nothing to host.
|
|
61
|
+
def hrl_office_qr_svg(company, brand)
|
|
62
|
+
text = [ brand, company[:address], company[:phone] ].compact_blank.join("\n")
|
|
63
|
+
RQRCode::QRCode.new(text).as_svg(use_path: true, viewbox: true, color: "0F172A").html_safe
|
|
64
|
+
end
|
|
65
|
+
|
|
57
66
|
# Only items whose routes exist yet (the nav grows with each phase).
|
|
58
67
|
def hrl_nav_items(items)
|
|
59
68
|
items.select { |item| hr_lite_route?(item[:path]) }
|
|
@@ -13,7 +13,7 @@ module HrLite
|
|
|
13
13
|
# profiles as roots (founders/directors).
|
|
14
14
|
belongs_to :manager, class_name: HrLite.config.user_class, optional: true
|
|
15
15
|
|
|
16
|
-
encrypts :pan_number, :pf_uan, :esi_number, :bank_account_number, :bank_ifsc
|
|
16
|
+
encrypts :pan_number, :pf_uan, :esi_number, :bank_account_number, :bank_ifsc, :blood_group, :emergency_contact
|
|
17
17
|
# fy_opening_*: income already paid this financial year that this install
|
|
18
18
|
# did not run — a previous employer, or the months before payroll was
|
|
19
19
|
# switched on here. Feeds the TDS projection so a mid-year start does not
|
|
@@ -21,12 +21,21 @@ module HrLite
|
|
|
21
21
|
encrypted_money :declared_annual_deductions, :fy_opening_gross, :fy_opening_tds
|
|
22
22
|
|
|
23
23
|
TAX_REGIMES = %w[new old].freeze
|
|
24
|
+
BLOOD_GROUPS = %w[A+ A- B+ B- AB+ AB- O+ O-].freeze
|
|
25
|
+
# The ID-card photo. Images a browser can print; content-sniffed like
|
|
26
|
+
# Document, so the check is on the real bytes.
|
|
27
|
+
PHOTO_TYPES = %w[image/jpeg image/png image/webp].freeze
|
|
28
|
+
PHOTO_MAX_BYTES = 5.megabytes
|
|
29
|
+
|
|
30
|
+
has_one_attached :photo
|
|
24
31
|
|
|
25
32
|
# Onboarding-form virtuals (the controller hands them to
|
|
26
33
|
# config.onboard_user; never persisted, never audited).
|
|
27
34
|
attr_accessor :new_user_name, :new_user_email, :new_user_password
|
|
28
35
|
|
|
29
36
|
before_validation :assign_employee_code, on: :create
|
|
37
|
+
# Without a role a new hire cannot see policies or raise an HR request.
|
|
38
|
+
after_create { (role = Role.find_by(name: Role::EMPLOYEE)) && RoleAssignment.find_or_create_by!(user_id:, role:) }
|
|
30
39
|
|
|
31
40
|
validates :employee_code, presence: true, uniqueness: true
|
|
32
41
|
validates :user_id, uniqueness: true
|
|
@@ -37,6 +46,8 @@ module HrLite
|
|
|
37
46
|
validates :bank_ifsc, format: { with: /\A[A-Z]{4}0[A-Z0-9]{6}\z/, message: "is not a valid IFSC" },
|
|
38
47
|
allow_blank: true
|
|
39
48
|
validates :pf_uan, format: { with: /\A\d{12}\z/, message: "must be 12 digits" }, allow_blank: true
|
|
49
|
+
validates :blood_group, inclusion: { in: BLOOD_GROUPS }, allow_blank: true
|
|
50
|
+
validate :photo_is_a_small_image
|
|
40
51
|
validate :exit_after_joining
|
|
41
52
|
validate :manager_chain_acyclic
|
|
42
53
|
validate :manager_is_active_staff, if: :manager_id_changed?
|
|
@@ -97,7 +108,7 @@ module HrLite
|
|
|
97
108
|
private
|
|
98
109
|
|
|
99
110
|
# Codes are system-assigned: Settings prefix + zero-padded next number
|
|
100
|
-
# (
|
|
111
|
+
# (ESA-000001, ESA-000002, ...). Scans the highest existing suffix for the
|
|
101
112
|
# CURRENT prefix so changing the prefix restarts a fresh sequence
|
|
102
113
|
# without colliding with history.
|
|
103
114
|
def assign_employee_code
|
|
@@ -108,7 +119,7 @@ module HrLite
|
|
|
108
119
|
.pluck(:employee_code)
|
|
109
120
|
.filter_map { |code| code.delete_prefix(prefix)[/\A\d+\z/]&.to_i }
|
|
110
121
|
.max || 0
|
|
111
|
-
self.employee_code = format("%s%
|
|
122
|
+
self.employee_code = format("%s%06d", prefix, last + 1)
|
|
112
123
|
end
|
|
113
124
|
|
|
114
125
|
def sanitize_sql_like(value)
|
|
@@ -140,6 +151,13 @@ module HrLite
|
|
|
140
151
|
end
|
|
141
152
|
end
|
|
142
153
|
|
|
154
|
+
def photo_is_a_small_image
|
|
155
|
+
return unless photo.attached?
|
|
156
|
+
return errors.add(:photo, "must be a JPG, PNG or WebP image") unless PHOTO_TYPES.include?(photo.blob.content_type)
|
|
157
|
+
|
|
158
|
+
errors.add(:photo, "must be under #{PHOTO_MAX_BYTES / 1.megabyte} MB") if photo.blob.byte_size > PHOTO_MAX_BYTES
|
|
159
|
+
end
|
|
160
|
+
|
|
143
161
|
def mask_middle(value)
|
|
144
162
|
return nil if value.blank?
|
|
145
163
|
return "•" * value.length if value.length <= 4
|
|
@@ -8,7 +8,7 @@ module HrLite
|
|
|
8
8
|
|
|
9
9
|
CATEGORIES = %w[
|
|
10
10
|
salary_certificate employment_certificate address_change bank_change
|
|
11
|
-
document_request payroll_query tax_query insurance_query policy_query other
|
|
11
|
+
document_request payroll_query tax_query insurance_query policy_query id_card other
|
|
12
12
|
].freeze
|
|
13
13
|
|
|
14
14
|
belongs_to :user, class_name: HrLite.config.user_class
|
|
@@ -17,6 +17,7 @@ module HrLite
|
|
|
17
17
|
validates :subject, presence: true
|
|
18
18
|
validates :category, inclusion: { in: CATEGORIES }
|
|
19
19
|
validates :status, inclusion: { in: STATUSES }
|
|
20
|
+
validate :id_card_can_be_printed, on: :create, if: -> { category == "id_card" }
|
|
20
21
|
|
|
21
22
|
scope :open_requests, -> { where(status: %w[open in_progress]) }
|
|
22
23
|
scope :recent_first, -> { order(created_at: :desc) }
|
|
@@ -25,7 +26,7 @@ module HrLite
|
|
|
25
26
|
|
|
26
27
|
after_create_commit :notify_desk
|
|
27
28
|
|
|
28
|
-
def category_label = category.humanize
|
|
29
|
+
def category_label = category.humanize.sub(/\AId\b/, "ID")
|
|
29
30
|
|
|
30
31
|
def assign!(actor:, assignee:)
|
|
31
32
|
update!(assigned_to_id: assignee.id, status: "in_progress")
|
|
@@ -68,8 +69,16 @@ module HrLite
|
|
|
68
69
|
Notifications.publish(
|
|
69
70
|
"hr_request.raised",
|
|
70
71
|
title: "#{HrLite.display_name(user)} asked: #{subject}",
|
|
71
|
-
body: body.presence, path: "/admin/hr_requests/#{id}", bell_to: desk
|
|
72
|
+
body: body.presence, path: "/admin/hr_requests/#{id}", bell_to: desk, email_to: desk
|
|
72
73
|
)
|
|
73
74
|
end
|
|
75
|
+
|
|
76
|
+
def id_card_can_be_printed
|
|
77
|
+
if !EmployeeProfile.find_by(user_id: user_id)&.photo&.attached?
|
|
78
|
+
errors.add(:base, "Add your photo to the ID card first")
|
|
79
|
+
elsif self.class.open_requests.exists?(user_id: user_id, category: "id_card")
|
|
80
|
+
errors.add(:base, "Your ID card print is already requested")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
74
83
|
end
|
|
75
84
|
end
|
|
@@ -7,6 +7,8 @@ module HrLite
|
|
|
7
7
|
belongs_to :user, class_name: HrLite.config.user_class
|
|
8
8
|
belongs_to :leave_type
|
|
9
9
|
belongs_to :decided_by, class_name: HrLite.config.user_class, optional: true
|
|
10
|
+
# Set when HR records leave the employee forgot to apply for.
|
|
11
|
+
belongs_to :created_by, class_name: HrLite.config.user_class, optional: true
|
|
10
12
|
|
|
11
13
|
validates :start_date, :end_date, presence: true
|
|
12
14
|
validates :status, inclusion: { in: STATUSES }
|
|
@@ -22,7 +24,7 @@ module HrLite
|
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
before_validation :cache_days_count, on: :create
|
|
25
|
-
after_create :notify_requested
|
|
27
|
+
after_create :notify_requested, unless: :recorded_by_hr?
|
|
26
28
|
|
|
27
29
|
scope :pending, -> { where(status: "pending") }
|
|
28
30
|
scope :approved, -> { where(status: "approved") }
|
|
@@ -209,13 +211,20 @@ module HrLite
|
|
|
209
211
|
LeaveDayCounter.count(self) > balance.available(as_of: start_date)
|
|
210
212
|
end
|
|
211
213
|
|
|
214
|
+
def recorded_by_hr? = created_by_id.present? && created_by_id != user_id
|
|
215
|
+
|
|
216
|
+
# Recorded leave is approved in the same transaction, so there is nothing
|
|
217
|
+
# left for a flow to route.
|
|
218
|
+
def open_approval_route = pending? && super
|
|
219
|
+
|
|
212
220
|
def notify_requested
|
|
213
221
|
Notifications.publish(
|
|
214
222
|
"leave.requested",
|
|
215
223
|
title: "#{HrLite.display_name(user)} applied for #{leave_type.name} (#{date_range_label})",
|
|
216
224
|
body: reason.presence,
|
|
217
225
|
path: "/admin/leave_requests/#{id}",
|
|
218
|
-
bell_to: HrLite.admin_users
|
|
226
|
+
bell_to: HrLite.admin_users,
|
|
227
|
+
email_to: HrLite.admin_users
|
|
219
228
|
)
|
|
220
229
|
end
|
|
221
230
|
|
|
@@ -224,6 +233,8 @@ module HrLite
|
|
|
224
233
|
# channel). Deliberately excludes the reason: dates are team-relevant,
|
|
225
234
|
# the why is not.
|
|
226
235
|
def notify_team
|
|
236
|
+
return if end_date < Date.current # nobody needs to hear who was out
|
|
237
|
+
|
|
227
238
|
team = HrLite.active_employees.reject { |member| member.id == user_id }
|
|
228
239
|
return if team.empty?
|
|
229
240
|
|
|
@@ -285,7 +296,7 @@ module HrLite
|
|
|
285
296
|
clash = self.class.where(user_id: user_id, status: %w[pending approved])
|
|
286
297
|
.where.not(id: id)
|
|
287
298
|
.overlapping_range(start_date, end_date)
|
|
288
|
-
errors.add(:base, "
|
|
299
|
+
errors.add(:base, "Leave already overlaps these dates") if clash.exists?
|
|
289
300
|
end
|
|
290
301
|
|
|
291
302
|
# A full-day leave over a day that already has a check-in makes no
|
|
@@ -296,7 +307,7 @@ module HrLite
|
|
|
296
307
|
|
|
297
308
|
punched = AttendanceRecord.where(user_id: user_id, date: start_date..end_date)
|
|
298
309
|
.where.not(check_in_at: nil)
|
|
299
|
-
errors.add(:base, "
|
|
310
|
+
errors.add(:base, "Attendance is marked in this period") if punched.exists?
|
|
300
311
|
end
|
|
301
312
|
|
|
302
313
|
def sufficient_balance
|
|
@@ -12,6 +12,8 @@ module HrLite
|
|
|
12
12
|
validates :effective_from, presence: true
|
|
13
13
|
validates :version, numericality: { greater_than: 0 }
|
|
14
14
|
|
|
15
|
+
after_create_commit :announce, if: :published?
|
|
16
|
+
|
|
15
17
|
scope :published, -> { where(published: true) }
|
|
16
18
|
scope :live_on, ->(date) { published.where(effective_from: ..date) }
|
|
17
19
|
scope :newest_first, -> { order(effective_from: :desc, version: :desc) }
|
|
@@ -34,6 +36,8 @@ module HrLite
|
|
|
34
36
|
)
|
|
35
37
|
end
|
|
36
38
|
|
|
39
|
+
def announcement? = !acknowledgement_required
|
|
40
|
+
|
|
37
41
|
def acknowledged_by?(user)
|
|
38
42
|
policy_acknowledgements.exists?(user_id: user.id)
|
|
39
43
|
end
|
|
@@ -55,5 +59,16 @@ module HrLite
|
|
|
55
59
|
rescue ActiveRecord::RecordNotUnique
|
|
56
60
|
policy_acknowledgements.find_by!(user_id: user.id)
|
|
57
61
|
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def announce
|
|
66
|
+
staff = HrLite.active_employees
|
|
67
|
+
Notifications.publish(
|
|
68
|
+
"policy.published",
|
|
69
|
+
title: "#{announcement? ? 'Announcement' : 'Policy'}: #{title}#{' (please read and acknowledge)' unless announcement?}",
|
|
70
|
+
body: body.truncate(300), path: "/policies/#{id}", bell_to: staff, email_to: staff
|
|
71
|
+
)
|
|
72
|
+
end
|
|
58
73
|
end
|
|
59
74
|
end
|
|
@@ -7,7 +7,7 @@ module HrLite
|
|
|
7
7
|
|
|
8
8
|
validates :weekend_policy, inclusion: { in: WEEKEND_POLICIES }
|
|
9
9
|
validates :employee_code_prefix, presence: true,
|
|
10
|
-
format: { with: /\A[A-Za-z]{1,10}
|
|
10
|
+
format: { with: /\A[A-Za-z]{1,10}-?\z/, message: "letters only (max 10), optionally ending in -" }
|
|
11
11
|
|
|
12
12
|
def self.instance
|
|
13
13
|
first_or_create!
|
|
@@ -57,6 +57,11 @@
|
|
|
57
57
|
<div class="hrl-field"><%= f.label :work_location %><%= f.text_field :work_location %></div>
|
|
58
58
|
<div class="hrl-field"><%= f.label :date_of_birth %><%= f.date_field :date_of_birth %></div>
|
|
59
59
|
<div class="hrl-field"><%= f.label :date_of_joining %><%= f.date_field :date_of_joining %></div>
|
|
60
|
+
<div class="hrl-field">
|
|
61
|
+
<%= f.label :blood_group %>
|
|
62
|
+
<%= f.select :blood_group, HrLite::EmployeeProfile::BLOOD_GROUPS, include_blank: "—" %>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="hrl-field"><%= f.label :emergency_contact, "Emergency contact (printed on the ID card)" %><%= f.telephone_field :emergency_contact %></div>
|
|
60
65
|
<div class="hrl-field">
|
|
61
66
|
<%= f.label :date_of_exit, "Date of exit (leave blank while employed)" %>
|
|
62
67
|
<%= f.date_field :date_of_exit %>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<h1 class="hrl-page__title"><%= hr_display_name(@profile.user) %> <span class="hrl-muted">(<%= @profile.employee_code %>)</span></h1>
|
|
4
4
|
<div class="hrl-page__actions">
|
|
5
5
|
<a class="hrl-btn" href="<%= hr_lite.edit_admin_employee_path(@profile) %>">Edit</a>
|
|
6
|
+
<a class="hrl-btn" href="<%= hr_lite.id_card_path(user_id: @profile.user_id) %>">ID card</a>
|
|
6
7
|
<% if hr_superadmin? %>
|
|
7
8
|
<a class="hrl-btn" href="<%= hr_lite.new_admin_employee_appraisal_path(@profile) %>">New appraisal</a>
|
|
8
9
|
<a class="hrl-btn" href="<%= hr_lite.new_admin_employee_designation_change_path(@profile) %>">Role change</a>
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
<%= @request.created_at.to_date.strftime("%d %b %Y") %> ·
|
|
8
8
|
<%= hrl_request_status_badge(@request.status) %>
|
|
9
9
|
</p>
|
|
10
|
+
<% if @request.category == "id_card" %>
|
|
11
|
+
<a class="hrl-btn hrl-mt" href="<%= hr_lite.id_card_path(user_id: @request.user_id) %>">Open the ID card to print</a>
|
|
12
|
+
<% end %>
|
|
10
13
|
<% if @request.body.present? %>
|
|
11
14
|
<div class="hrl-prose"><%= simple_format(@request.body) %></div>
|
|
12
15
|
<% end %>
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
<% content_for(:page_title) { "Leave approvals" } %>
|
|
2
2
|
<div class="hrl-page__head">
|
|
3
3
|
<h1 class="hrl-page__title">Leave approvals</h1>
|
|
4
|
+
<div class="hrl-page__actions">
|
|
5
|
+
<a class="hrl-btn" href="<%= hr_lite.new_admin_leave_request_path %>">Record leave</a>
|
|
6
|
+
</div>
|
|
4
7
|
</div>
|
|
5
8
|
|
|
6
9
|
<%= render "hr_lite/admin/shared/approvals_tabs", active: :leaves %>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Record leave" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Record leave for an employee</h1>
|
|
4
|
+
<p class="hrl-page__sub">For leave somebody took but never applied for. It is saved as approved, and they get an email.</p>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<section class="hrl-card">
|
|
8
|
+
<%= form_with model: @request, url: hr_lite.admin_leave_requests_path, method: :post, local: true do |f| %>
|
|
9
|
+
<%= render "hr_lite/shared/form_errors", record: @request %>
|
|
10
|
+
<div class="hrl-field">
|
|
11
|
+
<%= label_tag :employee_code, "Employee (type a name or code)" %>
|
|
12
|
+
<%= text_field_tag :employee_code, params[:employee_code], list: "hrl-employees", required: true, autocomplete: "off" %>
|
|
13
|
+
<datalist id="hrl-employees">
|
|
14
|
+
<% @people.each do |p| %>
|
|
15
|
+
<option value="<%= p.employee_code %>"><%= hr_display_name(p.user) %></option>
|
|
16
|
+
<% end %>
|
|
17
|
+
</datalist>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="hrl-field">
|
|
20
|
+
<%= f.label :leave_type_id, "Type" %>
|
|
21
|
+
<%= f.collection_select :leave_type_id, HrLite::LeaveType.active, :id, :name %>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="hrl-field"><%= f.label :start_date, "From" %><%= f.date_field :start_date %></div>
|
|
24
|
+
<div class="hrl-field"><%= f.label :end_date, "To" %><%= f.date_field :end_date %></div>
|
|
25
|
+
<div class="hrl-field hrl-field--check">
|
|
26
|
+
<%= f.check_box :half_day %>
|
|
27
|
+
<%= f.label :half_day, "Half day (single-day only)" %>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="hrl-field"><%= f.label :reason, "Note (optional)" %><%= f.text_area :reason %></div>
|
|
30
|
+
<div class="hrl-form-actions">
|
|
31
|
+
<%= f.submit "Record leave", class: "hrl-btn hrl-btn--primary" %>
|
|
32
|
+
<a class="hrl-btn" href="<%= hr_lite.admin_leave_requests_path %>">Cancel</a>
|
|
33
|
+
</div>
|
|
34
|
+
<% end %>
|
|
35
|
+
</section>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Policies and announcements" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Policies and announcements</h1>
|
|
4
|
+
<div class="hrl-page__actions"><a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_admin_policy_path %>">Publish</a></div>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<section class="hrl-card">
|
|
8
|
+
<% if @policies.empty? %>
|
|
9
|
+
<p class="hrl-muted">Nothing published yet.</p>
|
|
10
|
+
<% else %>
|
|
11
|
+
<div class="hrl-table-wrap">
|
|
12
|
+
<table class="hrl-table hrl-table--stack">
|
|
13
|
+
<thead><tr><th>Title</th><th>Kind</th><th>In force</th><th>Still to acknowledge</th></tr></thead>
|
|
14
|
+
<tbody>
|
|
15
|
+
<% @policies.each do |policy| %>
|
|
16
|
+
<tr>
|
|
17
|
+
<td data-label="Title"><%= policy.title %> <span class="hrl-badge hrl-badge--muted">v<%= policy.version %></span></td>
|
|
18
|
+
<td data-label="Kind"><%= policy.announcement? ? "Announcement" : "Policy" %></td>
|
|
19
|
+
<td data-label="In force"><%= policy.effective_from.strftime("%d %b %Y") %></td>
|
|
20
|
+
<td data-label="Still to acknowledge"><%= policy.announcement? ? "—" : policy.outstanding_for.count %></td>
|
|
21
|
+
</tr>
|
|
22
|
+
<% end %>
|
|
23
|
+
</tbody>
|
|
24
|
+
</table>
|
|
25
|
+
</div>
|
|
26
|
+
<% end %>
|
|
27
|
+
</section>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Publish" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Publish a policy or announcement</h1>
|
|
4
|
+
<p class="hrl-page__sub">Every employee gets an email and a notification. Reusing a title publishes the next version and asks everyone again.</p>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<section class="hrl-card">
|
|
8
|
+
<%= form_with model: @policy, url: hr_lite.admin_policies_path, method: :post, local: true do |f| %>
|
|
9
|
+
<%= render "hr_lite/shared/form_errors", record: @policy %>
|
|
10
|
+
<div class="hrl-field"><%= f.label :title %><%= f.text_field :title, required: true %></div>
|
|
11
|
+
<div class="hrl-field"><%= f.label :body, "Text" %><%= f.text_area :body, rows: 10, required: true %></div>
|
|
12
|
+
<div class="hrl-field"><%= f.label :effective_from, "In force from" %><%= f.date_field :effective_from %></div>
|
|
13
|
+
<div class="hrl-field hrl-field--check">
|
|
14
|
+
<%= f.check_box :acknowledgement_required %>
|
|
15
|
+
<%= f.label :acknowledgement_required, "Policy — everyone must acknowledge it (untick for an announcement)" %>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="hrl-form-actions">
|
|
18
|
+
<%= f.submit "Publish", class: "hrl-btn hrl-btn--primary" %>
|
|
19
|
+
<a class="hrl-btn" href="<%= hr_lite.admin_policies_path %>">Cancel</a>
|
|
20
|
+
</div>
|
|
21
|
+
<% end %>
|
|
22
|
+
</section>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<% content_for(:page_title) { "ID card" } %>
|
|
2
|
+
<% company = HrLite.config.company.call %>
|
|
3
|
+
<% brand = company[:brand].presence || company[:name].presence || "HR" %>
|
|
4
|
+
<% own = @user == hr_current_user %>
|
|
5
|
+
<div class="hrl-page__head">
|
|
6
|
+
<h1 class="hrl-page__title"><%= own ? "My ID card" : "ID card — #{hr_display_name(@user)}" %></h1>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<% if @profile.nil? %>
|
|
10
|
+
<p class="hrl-card hrl-card--empty"><%= own ? "Your HR profile has not been set up yet, so there is no card to show." : "No HR profile yet." %></p>
|
|
11
|
+
<% else %>
|
|
12
|
+
<% first, rest = hr_display_name(@user).to_s.split(" ", 2) %>
|
|
13
|
+
<div class="hrl-idcard-faces">
|
|
14
|
+
<div class="hrl-idcard-face">
|
|
15
|
+
<span class="hrl-idcard__label">FRONT</span>
|
|
16
|
+
<div class="hrl-idcard">
|
|
17
|
+
<div class="hrl-idcard__band">
|
|
18
|
+
<span class="hrl-idcard__slot"></span>
|
|
19
|
+
<span class="hrl-idcard__logo">
|
|
20
|
+
<% if company[:logo_url].present? %><img src="<%= company[:logo_url] %>" alt="<%= brand %>"><% else %><%= brand %><% end %>
|
|
21
|
+
</span>
|
|
22
|
+
<span class="hrl-idcard__kicker">EMPLOYEE IDENTITY CARD</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="hrl-idcard__photo">
|
|
25
|
+
<% if @profile.photo.attached? %>
|
|
26
|
+
<img src="<%= Rails.application.routes.url_helpers.rails_blob_path(@profile.photo, disposition: "inline", only_path: true) %>" alt="<%= hr_display_name(@user) %>">
|
|
27
|
+
<% else %>
|
|
28
|
+
<span><%= hr_display_name(@user).to_s.scan(/\b[[:alpha:]]/).first(2).join.upcase %></span>
|
|
29
|
+
<% end %>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="hrl-idcard__who">
|
|
32
|
+
<div class="hrl-idcard__name"><span class="hrl-idcard__green"><%= first %></span> <span class="hrl-idcard__blue"><%= rest %></span></div>
|
|
33
|
+
<div class="hrl-idcard__role"><%= @profile.designation %></div>
|
|
34
|
+
</div>
|
|
35
|
+
<dl class="hrl-idcard__facts">
|
|
36
|
+
<div><dt>EMPLOYEE ID</dt><dd><%= @profile.employee_code %></dd></div>
|
|
37
|
+
<div><dt>DEPARTMENT</dt><dd><%= @profile.department.presence || "—" %></dd></div>
|
|
38
|
+
<div><dt>BLOOD GROUP</dt><dd class="hrl-idcard__red"><%= @profile.blood_group.presence || "—" %></dd></div>
|
|
39
|
+
<div><dt>JOINED</dt><dd><%= @profile.date_of_joining.strftime("%b %Y") %></dd></div>
|
|
40
|
+
</dl>
|
|
41
|
+
<div class="hrl-idcard__strip"><span></span><span></span><span></span></div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="hrl-idcard-face">
|
|
46
|
+
<span class="hrl-idcard__label">BACK</span>
|
|
47
|
+
<div class="hrl-idcard hrl-idcard--back">
|
|
48
|
+
<span class="hrl-idcard__slot hrl-idcard__slot--back"></span>
|
|
49
|
+
<div class="hrl-idcard__title"><span class="hrl-idcard__green">Scan to</span> <span class="hrl-idcard__blue">Verify</span></div>
|
|
50
|
+
<div class="hrl-idcard__qr"><%= hrl_office_qr_svg(company, brand) %></div>
|
|
51
|
+
<div class="hrl-idcard__code"><%= @profile.employee_code %></div>
|
|
52
|
+
<div class="hrl-idcard__rows">
|
|
53
|
+
<div><span>Emergency contact</span><b><%= @profile.emergency_contact.presence || "—" %></b></div>
|
|
54
|
+
<hr>
|
|
55
|
+
<div><span>Issued</span><b><%= @issued_on&.strftime("%b %Y") || "Not yet issued" %></b></div>
|
|
56
|
+
</div>
|
|
57
|
+
<p class="hrl-idcard__note">This card is the property of <%= brand %>. If found, please return it to our office<%= " or call #{company[:phone]}" if company[:phone].present? %>.</p>
|
|
58
|
+
<div class="hrl-idcard__foot"><b><%= brand %></b><span>AUTHORISED SIGNATORY</span></div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div class="hrl-idcard-actions">
|
|
64
|
+
<button type="button" class="hrl-btn hrl-btn--primary" data-hrl-print>Print</button>
|
|
65
|
+
<% if own && @print_requested %>
|
|
66
|
+
<p class="hrl-small hrl-muted">Printed card requested on <%= @print_requested.to_date.strftime("%d %b %Y") %>.</p>
|
|
67
|
+
<% elsif own %>
|
|
68
|
+
<%= button_to "Request printed card", hr_lite.hr_requests_path, class: "hrl-btn",
|
|
69
|
+
params: { hr_request: { category: "id_card", subject: "Please print my ID card" } } %>
|
|
70
|
+
<% end %>
|
|
71
|
+
<% if own %>
|
|
72
|
+
<%= form_with url: hr_lite.id_card_photo_path, method: :patch, multipart: true, class: "hrl-row" do |f| %>
|
|
73
|
+
<div class="hrl-field">
|
|
74
|
+
<label for="photo">Change photo</label>
|
|
75
|
+
<%= f.file_field :photo, id: "photo", accept: HrLite::EmployeeProfile::PHOTO_TYPES.join(","), required: true %>
|
|
76
|
+
<p class="hrl-hint">Face centred, plain background. JPG, PNG or WebP, under <%= HrLite::EmployeeProfile::PHOTO_MAX_BYTES / 1.megabyte %> MB.</p>
|
|
77
|
+
</div>
|
|
78
|
+
<%= f.submit "Upload photo", class: "hrl-btn" %>
|
|
79
|
+
<% end %>
|
|
80
|
+
<% end %>
|
|
81
|
+
</div>
|
|
82
|
+
<% end %>
|
|
83
|
+
|
|
84
|
+
<% content_for :scripts do %>
|
|
85
|
+
<%= javascript_include_tag "hr_lite/print", defer: true %>
|
|
86
|
+
<% end %>
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
|
|
57
57
|
<div class="hrl-side__foot">
|
|
58
58
|
<span class="hrl-side__user" title="<%= hr_current_user&.email %>"><%= hr_display_name(hr_current_user) %></span>
|
|
59
|
+
<%= render "hr_lite/shared/bell" %>
|
|
59
60
|
<% if HrLite.config.back_link %>
|
|
60
61
|
<a class="hrl-side__back" href="<%= HrLite.config.back_link[:url] %>"><%= HrLite.config.back_link[:label] %></a>
|
|
61
62
|
<% end %>
|
|
@@ -71,6 +72,7 @@
|
|
|
71
72
|
<%= company[:name].presence || "HR" %>
|
|
72
73
|
<% end %>
|
|
73
74
|
</a>
|
|
75
|
+
<%= render "hr_lite/shared/bell" %>
|
|
74
76
|
<span class="hrl-appbar__user"><%= hr_display_name(hr_current_user) %></span>
|
|
75
77
|
</header>
|
|
76
78
|
|
data/config/routes.rb
CHANGED
|
@@ -43,6 +43,8 @@ HrLite::Engine.routes.draw do
|
|
|
43
43
|
get "calendar", to: "calendar#show"
|
|
44
44
|
resources :salary_slips, only: %i[index show]
|
|
45
45
|
resource :employee_profile, only: :show, path: "profile"
|
|
46
|
+
patch "id_card/photo", to: "id_cards#photo", as: :id_card_photo
|
|
47
|
+
get "id_card(/:user_id)", to: "id_cards#show", as: :id_card
|
|
46
48
|
resource :resignation, only: %i[show create], controller: "resignations" do
|
|
47
49
|
post :withdraw
|
|
48
50
|
end
|
|
@@ -50,9 +52,10 @@ HrLite::Engine.routes.draw do
|
|
|
50
52
|
resources :appraisals, only: %i[index show]
|
|
51
53
|
|
|
52
54
|
namespace :admin do
|
|
55
|
+
resources :policies, only: %i[index new create]
|
|
53
56
|
get "overview", to: "overview#index"
|
|
54
57
|
resources :attendances, only: %i[index show update], param: :user_id
|
|
55
|
-
resources :leave_requests, only: %i[index show] do
|
|
58
|
+
resources :leave_requests, only: %i[index show new create] do
|
|
56
59
|
member { post :approve; post :reject; post :cancel }
|
|
57
60
|
end
|
|
58
61
|
resources :leave_balances, only: :index do
|
|
@@ -9,7 +9,10 @@ module HrLite
|
|
|
9
9
|
:time_zone, :currency_symbol, :on_designation_change,
|
|
10
10
|
:leadership_emails, :extra_stylesheets, :superadmin_emails,
|
|
11
11
|
:mailer_from, :public_url_base, :notification_matrix, :back_link,
|
|
12
|
-
:onboard_user, :offboard_user, :invite_url_for
|
|
12
|
+
:onboard_user, :offboard_user, :invite_url_for,
|
|
13
|
+
# ->(user) { { url:, unread: } } — shows a notifications link in
|
|
14
|
+
# the HR shell; nil (default) shows nothing.
|
|
15
|
+
:notifications
|
|
13
16
|
|
|
14
17
|
attr_reader :leave_year_start_month
|
|
15
18
|
|
|
@@ -12,7 +12,7 @@ module HrLite
|
|
|
12
12
|
# the matrix is purely the on/off routing table.
|
|
13
13
|
module Notifications
|
|
14
14
|
DEFAULT_MATRIX = {
|
|
15
|
-
"leave.requested" => { bell: true, email:
|
|
15
|
+
"leave.requested" => { bell: true, email: true, leadership_email: true, leadership_bell: true },
|
|
16
16
|
"leave.approved" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
|
|
17
17
|
"leave.rejected" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
|
|
18
18
|
"leave.cancelled" => { bell: true, email: false, leadership_email: true, leadership_bell: false },
|
|
@@ -38,17 +38,18 @@ module HrLite
|
|
|
38
38
|
"expense.approved" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
39
39
|
"expense.rejected" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
40
40
|
"expense.reimbursed" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
41
|
-
"hr_request.raised" => { bell: true, email:
|
|
41
|
+
"hr_request.raised" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
42
42
|
"hr_request.assigned" => { bell: true, email: false, leadership_email: false, leadership_bell: false },
|
|
43
43
|
"hr_request.resolved" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
44
44
|
"policy.published" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
45
|
+
"holiday.published" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
45
46
|
"digest.daily" => { bell: false, email: false, leadership_email: true, leadership_bell: false },
|
|
46
47
|
"resignation.submitted" => { bell: true, email: false, leadership_email: true, leadership_bell: true },
|
|
47
48
|
"resignation.accepted" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
|
|
48
49
|
"resignation.withdrawn" => { bell: true, email: false, leadership_email: true, leadership_bell: false },
|
|
49
50
|
"employee.onboarded" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
|
|
50
51
|
"payroll.draft_ready" => { bell: false, email: false, leadership_email: true, leadership_bell: true },
|
|
51
|
-
"leave.team_notice" => { bell: true, email:
|
|
52
|
+
"leave.team_notice" => { bell: true, email: false, leadership_email: false, leadership_bell: false },
|
|
52
53
|
"comp_off.requested" => { bell: true, email: false, leadership_email: true, leadership_bell: true },
|
|
53
54
|
"comp_off.approved" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
|
|
54
55
|
"comp_off.rejected" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
data/lib/hr_lite/version.rb
CHANGED
data/lib/hr_lite.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hr_lite
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.16.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kshitiz sinha
|
|
@@ -37,6 +37,20 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rqrcode
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.0'
|
|
40
54
|
description: Mountable Rails engine providing attendance with geolocation, leave management,
|
|
41
55
|
holiday calendar, Indian payroll (PF/ESI/PT/TDS) with salary-slip PDFs, kudos with
|
|
42
56
|
@mentions, appraisals and promotions — integrated into a host app via config hooks.
|
|
@@ -53,6 +67,7 @@ files:
|
|
|
53
67
|
- app/assets/javascripts/hr_lite/confirm.js
|
|
54
68
|
- app/assets/javascripts/hr_lite/geo_punch.js
|
|
55
69
|
- app/assets/javascripts/hr_lite/mention.js
|
|
70
|
+
- app/assets/javascripts/hr_lite/print.js
|
|
56
71
|
- app/assets/stylesheets/hr_lite/application.css
|
|
57
72
|
- app/assets/stylesheets/hr_lite/hr_lite.css
|
|
58
73
|
- app/controllers/hr_lite/admin/appraisals_controller.rb
|
|
@@ -76,6 +91,7 @@ files:
|
|
|
76
91
|
- app/controllers/hr_lite/admin/office_locations_controller.rb
|
|
77
92
|
- app/controllers/hr_lite/admin/overview_controller.rb
|
|
78
93
|
- app/controllers/hr_lite/admin/payroll_runs_controller.rb
|
|
94
|
+
- app/controllers/hr_lite/admin/policies_controller.rb
|
|
79
95
|
- app/controllers/hr_lite/admin/regularization_requests_controller.rb
|
|
80
96
|
- app/controllers/hr_lite/admin/reports_controller.rb
|
|
81
97
|
- app/controllers/hr_lite/admin/resignations_controller.rb
|
|
@@ -101,6 +117,7 @@ files:
|
|
|
101
117
|
- app/controllers/hr_lite/holidays_controller.rb
|
|
102
118
|
- app/controllers/hr_lite/home_controller.rb
|
|
103
119
|
- app/controllers/hr_lite/hr_requests_controller.rb
|
|
120
|
+
- app/controllers/hr_lite/id_cards_controller.rb
|
|
104
121
|
- app/controllers/hr_lite/kudos_controller.rb
|
|
105
122
|
- app/controllers/hr_lite/leave_balances_controller.rb
|
|
106
123
|
- app/controllers/hr_lite/leave_requests_controller.rb
|
|
@@ -214,6 +231,7 @@ files:
|
|
|
214
231
|
- app/views/hr_lite/admin/hr_requests/show.html.erb
|
|
215
232
|
- app/views/hr_lite/admin/leave_balances/index.html.erb
|
|
216
233
|
- app/views/hr_lite/admin/leave_requests/index.html.erb
|
|
234
|
+
- app/views/hr_lite/admin/leave_requests/new.html.erb
|
|
217
235
|
- app/views/hr_lite/admin/leave_requests/show.html.erb
|
|
218
236
|
- app/views/hr_lite/admin/leave_types/_form.html.erb
|
|
219
237
|
- app/views/hr_lite/admin/leave_types/edit.html.erb
|
|
@@ -230,6 +248,8 @@ files:
|
|
|
230
248
|
- app/views/hr_lite/admin/payroll_runs/index.html.erb
|
|
231
249
|
- app/views/hr_lite/admin/payroll_runs/new.html.erb
|
|
232
250
|
- app/views/hr_lite/admin/payroll_runs/show.html.erb
|
|
251
|
+
- app/views/hr_lite/admin/policies/index.html.erb
|
|
252
|
+
- app/views/hr_lite/admin/policies/new.html.erb
|
|
233
253
|
- app/views/hr_lite/admin/regularization_requests/index.html.erb
|
|
234
254
|
- app/views/hr_lite/admin/regularization_requests/show.html.erb
|
|
235
255
|
- app/views/hr_lite/admin/reports/index.html.erb
|
|
@@ -275,6 +295,7 @@ files:
|
|
|
275
295
|
- app/views/hr_lite/hr_requests/index.html.erb
|
|
276
296
|
- app/views/hr_lite/hr_requests/new.html.erb
|
|
277
297
|
- app/views/hr_lite/hr_requests/show.html.erb
|
|
298
|
+
- app/views/hr_lite/id_cards/show.html.erb
|
|
278
299
|
- app/views/hr_lite/kudos/_kudo.html.erb
|
|
279
300
|
- app/views/hr_lite/kudos/index.html.erb
|
|
280
301
|
- app/views/hr_lite/leave_balances/index.html.erb
|
|
@@ -294,6 +315,7 @@ files:
|
|
|
294
315
|
- app/views/hr_lite/salary_slips/index.html.erb
|
|
295
316
|
- app/views/hr_lite/salary_slips/pdf.html.erb
|
|
296
317
|
- app/views/hr_lite/salary_slips/show.html.erb
|
|
318
|
+
- app/views/hr_lite/shared/_bell.html.erb
|
|
297
319
|
- app/views/hr_lite/shared/_form_errors.html.erb
|
|
298
320
|
- app/views/hr_lite/shared/_pagination.html.erb
|
|
299
321
|
- app/views/hr_lite/tax_declarations/show.html.erb
|
|
@@ -337,6 +359,8 @@ files:
|
|
|
337
359
|
- db/migrate/20260818003616_create_hr_lite_documents_and_declarations.rb
|
|
338
360
|
- db/migrate/20260818004707_create_hr_lite_employee_services.rb
|
|
339
361
|
- db/migrate/20260818010401_create_hr_lite_assets_and_onboarding.rb
|
|
362
|
+
- db/migrate/20260927100145_add_id_card_fields_to_hr_lite_employee_profiles.rb
|
|
363
|
+
- db/migrate/20260927101031_add_created_by_id_to_hr_lite_leave_requests.rb
|
|
340
364
|
- lib/generators/hr_lite/install/install_generator.rb
|
|
341
365
|
- lib/generators/hr_lite/install/templates/AFTER_INSTALL
|
|
342
366
|
- lib/generators/hr_lite/install/templates/initializer.rb
|