hr_lite 0.22.0 → 0.23.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 +18 -0
- data/README.md +10 -0
- data/app/assets/javascripts/hr_lite/geo_punch.js +64 -9
- data/app/assets/stylesheets/hr_lite/hr_lite.css +20 -0
- data/app/controllers/hr_lite/admin/awards_controller.rb +43 -0
- data/app/controllers/hr_lite/admin/performance_plans_controller.rb +46 -0
- data/app/controllers/hr_lite/home_controller.rb +1 -0
- data/app/controllers/hr_lite/timelines_controller.rb +19 -0
- data/app/helpers/hr_lite/application_helper.rb +4 -1
- data/app/models/hr_lite/audit_log.rb +1 -0
- data/app/models/hr_lite/award.rb +50 -0
- data/app/models/hr_lite/performance_plan.rb +43 -0
- data/app/services/hr_lite/timeline.rb +183 -0
- data/app/views/hr_lite/admin/awards/_form.html.erb +24 -0
- data/app/views/hr_lite/admin/awards/edit.html.erb +3 -0
- data/app/views/hr_lite/admin/awards/index.html.erb +30 -0
- data/app/views/hr_lite/admin/awards/new.html.erb +3 -0
- data/app/views/hr_lite/admin/employees/show.html.erb +2 -0
- data/app/views/hr_lite/admin/performance_plans/_form.html.erb +29 -0
- data/app/views/hr_lite/admin/performance_plans/edit.html.erb +3 -0
- data/app/views/hr_lite/admin/performance_plans/index.html.erb +30 -0
- data/app/views/hr_lite/admin/performance_plans/new.html.erb +3 -0
- data/app/views/hr_lite/attendance/_punch_card.html.erb +3 -3
- data/app/views/hr_lite/home/index.html.erb +19 -0
- data/app/views/hr_lite/org/_node.html.erb +1 -1
- data/app/views/hr_lite/timelines/_events.html.erb +20 -0
- data/app/views/hr_lite/timelines/show.html.erb +36 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20261010090000_create_hr_lite_performance_plans.rb +22 -0
- data/db/migrate/20261010090100_create_hr_lite_awards.rb +17 -0
- data/lib/hr_lite/notifications.rb +3 -0
- data/lib/hr_lite/version.rb +1 -1
- metadata +19 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8e83919f345e7b70bbe33b17937f9b44c70b04d1d58919d3d24607ac3931ba86
|
|
4
|
+
data.tar.gz: '0973e68fde9009f3b6e0e47de86a06149a97ed0158bc5c289740b5469192c82e'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 62e9b4d98c7f8f3743129b731d960c0879f9c71aa808536e5fde70c16f6f20eb6c0c26960cfa5c2cfd29396dfdfcbd22dab3a6329f482e39e35795df5cea9727
|
|
7
|
+
data.tar.gz: 07dcf6f864bc9675f5710b887ac7879f2996f22c926d64e3672d8b7d97f191d50bdedc4c7a190784c81525b87baf5acb21e9aff04fd8b208637d15a497d58231
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.23.0] - 2026-10-10
|
|
11
|
+
|
|
12
|
+
Two migrations: `hr_lite_performance_plans` and `hr_lite_awards`. No new permission keys.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Employee timeline: `/timeline` (your own) and `/people/:user_id/timeline` (anyone's), with a year picker; every name on the org chart links to it, and the admin employee page has a Timeline button. A header card gives role, department, joining date and tenure. Below it, two cards on a dot-and-rail list grouped by month, each event labelled in words (Leave, Promotion, Kudos, Award, Pay, Performance…). "Public timeline", which everyone sees: joining and exit, role changes in effect, kudos received and given, awards, and approved leave (colleagues see "On leave" without the type). "Private records", on a shaded card with a dashed rail and hollow dots, shown only to the employee and to whoever the permission reaches, and absent altogether for anyone else: bonus, incentive and arrears items and loans (`payroll.view`), approved and reimbursed expenses (`expense.approve`), shared appraisals and PIPs (`appraisal.view`), approved attendance fixes (`attendance.view`), comp-off earned (`leave.view`), assets given and returned (`asset.view`), verified documents (each document's own `readable_by?`), resignation filed and accepted (`resignation.view`), probation end (`profile.view`) and resolved help-desk requests (`hr_request.manage`). New CSS: `.hrl-timeline`, `.hrl-timeline__month`, `.hrl-timeline__item`, `.hrl-timeline__title`, `.hrl-card--private`. `HrLite::Timeline` reads existing records only.
|
|
16
|
+
- Performance improvement plans (`HrLite::PerformancePlan`): Super Admin starts one at Organisation → PIPs (`/admin/performance_plans`, or "Start PIP" on the employee page) with goals and a review date, then marks it extended, passed or failed; passed or failed stamps `closed_at` and freezes the plan. The employee hears `pip.started` and `pip.closed` (bell and email, never leadership). PIPs are money tier in the audit trail, like appraisals.
|
|
17
|
+
- Employee of the Month, Quarter and Year (`HrLite::Award`): Leadership picks the winner and writes a citation at Organisation → Awards (`/admin/awards`); one winner per period, saved as the period's first day. The winner hears `award.won`, and a Home card shows the current winner of each to everyone.
|
|
18
|
+
- Deviation from the plan: PIPs sit with appraisals under the Super Admin screens (`SuperadminController`) and Awards under Leadership (`LeadershipController`), rather than new permission keys, so existing installs need no role changes.
|
|
19
|
+
|
|
20
|
+
## [0.22.1] - 2026-10-09
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- Location help on the punch card: when a punch has no GPS, the card lists the steps to turn location on for the device in use (iPhone and iPad in Safari, Chrome, Firefox or Edge; Android in Chrome or Samsung Internet; desktop Chrome, Edge, Brave, Firefox or Safari, plus the macOS or Windows location switch). A web page cannot open device settings, so the steps say where to tap. New `.hrl-geo-help` list.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- With `config.require_punch_location`, the punch form carries `data-hrl-geo-required` and the browser holds a timed-out or unavailable fix on the page, with the same steps, instead of submitting a punch the server refuses.
|
|
27
|
+
|
|
10
28
|
## [0.22.0] - 2026-10-09
|
|
11
29
|
|
|
12
30
|
The HRMS UI/UX pass: Keka/greytHR patterns and a ui-ux-pro-max review of every screen, phone first. No migrations. Hosts that theme through `--hrl-*` variables keep their values; the engine defaults for radius, info background and muted text change (see Changed).
|
data/README.md
CHANGED
|
@@ -363,6 +363,16 @@ visible to everyone and shows the reporting tree — names, designations and
|
|
|
363
363
|
departments only, never salary or private data — with each viewer's own
|
|
364
364
|
reporting line labelled L1/L2/…
|
|
365
365
|
|
|
366
|
+
Every employee has a timeline (`/timeline`, a colleague's at
|
|
367
|
+
`/people/:user_id/timeline`, linked from the org chart): joining and exit,
|
|
368
|
+
role changes, kudos received, awards and leave dates are public; leave type,
|
|
369
|
+
bonus and other pay items, loans, expenses, appraisals and performance
|
|
370
|
+
improvement plans show only to the employee and to whoever the matching
|
|
371
|
+
permission reaches (`leave.view`, `payroll.view`, `expense.approve`,
|
|
372
|
+
`appraisal.view`). Super Admin writes PIPs at `/admin/performance_plans`;
|
|
373
|
+
Leadership picks Employee of the Month, Quarter and Year at `/admin/awards`,
|
|
374
|
+
and Home shows the current winners to everyone.
|
|
375
|
+
|
|
366
376
|
## Recurring jobs
|
|
367
377
|
|
|
368
378
|
Schedule these on the host's job scheduler (cron, GoodJob, Sidekiq-cron,
|
|
@@ -1,16 +1,64 @@
|
|
|
1
1
|
// Dependency-free geolocation capture for punch forms.
|
|
2
2
|
// Markup contract:
|
|
3
|
-
// <form data-hrl-geo-punch>
|
|
3
|
+
// <form data-hrl-geo-punch [data-hrl-geo-required]>
|
|
4
4
|
// <input type="hidden" name="lat"><input type="hidden" name="lng">
|
|
5
5
|
// <input type="hidden" name="accuracy_m"><input type="hidden" name="geo_status">
|
|
6
6
|
// <button type="submit">…</button>
|
|
7
7
|
// <p data-hrl-geo-status role="status" aria-live="polite"></p> (not hidden, or the first message is missed)
|
|
8
8
|
// </form>
|
|
9
9
|
// A denied permission holds the punch; timeout/unavailable submit anyway
|
|
10
|
-
// with geo_status set
|
|
10
|
+
// with geo_status set (the server flags them) — unless the form is marked
|
|
11
|
+
// data-hrl-geo-required, which holds those too. A held punch shows how to
|
|
12
|
+
// turn location on for this device: no web page can open device settings.
|
|
11
13
|
(function () {
|
|
12
14
|
"use strict";
|
|
13
15
|
|
|
16
|
+
// Steps per device, matched on the user agent. iPadOS reports a Mac UA, so a
|
|
17
|
+
// touch-capable "Mac" counts as iOS.
|
|
18
|
+
function locationSteps() {
|
|
19
|
+
var ua = navigator.userAgent;
|
|
20
|
+
var ios = /iPhone|iPad|iPod/.test(ua) || (/Macintosh/.test(ua) && navigator.maxTouchPoints > 1);
|
|
21
|
+
if (ios) {
|
|
22
|
+
var app = /CriOS/.test(ua) ? "Chrome" : /FxiOS/.test(ua) ? "Firefox" : /EdgiOS/.test(ua) ? "Edge" : null;
|
|
23
|
+
return [
|
|
24
|
+
"Open Settings → Privacy & Security → Location Services and turn it on.",
|
|
25
|
+
app ? "In Settings, open Apps → " + app + " → Location and choose While Using the App."
|
|
26
|
+
: "In Settings, open Apps → Safari → Location and choose Allow (or Ask).",
|
|
27
|
+
"Come back here, reload the page and tap again."
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
if (/Android/.test(ua)) {
|
|
31
|
+
var samsung = /SamsungBrowser/.test(ua);
|
|
32
|
+
return [
|
|
33
|
+
"Swipe down from the top of the screen and turn Location on.",
|
|
34
|
+
samsung ? "In Samsung Internet, tap ☰ → Settings → Sites and downloads → Site permissions → Location, and allow this site."
|
|
35
|
+
: "Tap the icon left of the web address → Permissions → Location → Allow. (Or ⋮ → Settings → Site settings → Location.)",
|
|
36
|
+
"Reload the page and tap again."
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
var steps = [];
|
|
40
|
+
if (/Firefox/.test(ua)) steps.push("Click the icon left of the web address and remove the blocked Location permission.");
|
|
41
|
+
else if (/Safari/.test(ua) && !/Chrome|Chromium|Edg/.test(ua)) steps.push("Open Safari → Settings → Websites → Location and set this site to Allow.");
|
|
42
|
+
else steps.push("Click the icon left of the web address → Location → Allow.");
|
|
43
|
+
if (/Mac OS X|Macintosh/.test(ua)) steps.push("On the Mac, open System Settings → Privacy & Security → Location Services and turn it on for this browser.");
|
|
44
|
+
else if (/Windows/.test(ua)) steps.push("In Windows, open Settings → Privacy & security → Location and turn on location access.");
|
|
45
|
+
steps.push("Reload the page and tap again.");
|
|
46
|
+
return steps;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function showHelp(label, headline) {
|
|
50
|
+
if (!label) return;
|
|
51
|
+
label.textContent = headline;
|
|
52
|
+
var list = document.createElement("ol");
|
|
53
|
+
list.className = "hrl-geo-help";
|
|
54
|
+
locationSteps().forEach(function (step) {
|
|
55
|
+
var item = document.createElement("li");
|
|
56
|
+
item.textContent = step;
|
|
57
|
+
list.appendChild(item);
|
|
58
|
+
});
|
|
59
|
+
label.appendChild(list);
|
|
60
|
+
}
|
|
61
|
+
|
|
14
62
|
function setup(form) {
|
|
15
63
|
var fields = {
|
|
16
64
|
lat: form.querySelector("[name=lat]"),
|
|
@@ -20,6 +68,7 @@
|
|
|
20
68
|
};
|
|
21
69
|
var button = form.querySelector("[type=submit]");
|
|
22
70
|
var label = form.querySelector("[data-hrl-geo-status]");
|
|
71
|
+
var required = form.hasAttribute("data-hrl-geo-required");
|
|
23
72
|
var locating = false;
|
|
24
73
|
|
|
25
74
|
function send(status) {
|
|
@@ -28,6 +77,12 @@
|
|
|
28
77
|
form.submit();
|
|
29
78
|
}
|
|
30
79
|
|
|
80
|
+
function hold(headline) {
|
|
81
|
+
locating = false;
|
|
82
|
+
if (button) { button.disabled = false; button.removeAttribute("aria-busy"); }
|
|
83
|
+
showHelp(label, headline);
|
|
84
|
+
}
|
|
85
|
+
|
|
31
86
|
form.addEventListener("submit", function (event) {
|
|
32
87
|
if (locating || fields.status.value) return; // second pass: let it through
|
|
33
88
|
event.preventDefault();
|
|
@@ -35,7 +90,11 @@
|
|
|
35
90
|
if (button) { button.disabled = true; button.setAttribute("aria-busy", "true"); }
|
|
36
91
|
if (label) { label.hidden = false; label.textContent = "Getting location…"; }
|
|
37
92
|
|
|
38
|
-
if (!navigator.geolocation) {
|
|
93
|
+
if (!navigator.geolocation) {
|
|
94
|
+
if (required) hold("This browser cannot share your location. Open this page in Chrome or Safari.");
|
|
95
|
+
else send("unavailable");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
39
98
|
|
|
40
99
|
navigator.geolocation.getCurrentPosition(
|
|
41
100
|
function (pos) {
|
|
@@ -47,12 +106,8 @@
|
|
|
47
106
|
function (err) {
|
|
48
107
|
// Browsers ask only once; a past "Don't allow" denies silently forever.
|
|
49
108
|
// Hold the punch so it is never filed (and flagged) without GPS.
|
|
50
|
-
if (err && err.code === 1) {
|
|
51
|
-
|
|
52
|
-
if (button) { button.disabled = false; button.removeAttribute("aria-busy"); }
|
|
53
|
-
if (label) label.textContent = "Location is blocked. Allow location for this site in your browser settings, then tap again.";
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
109
|
+
if (err && err.code === 1) { hold("Location is blocked. To allow it:"); return; }
|
|
110
|
+
if (required) { hold("Couldn't get your location. Check that location is on:"); return; }
|
|
56
111
|
send("timeout");
|
|
57
112
|
},
|
|
58
113
|
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 }
|
|
@@ -240,6 +240,8 @@ html:has(.hrl-body) { scroll-padding-top: 4rem; scroll-padding-bottom: calc(4.4r
|
|
|
240
240
|
.hrl-listrow > :first-child { display: inline-flex; align-items: center; gap: var(--hrl-s-1); line-height: var(--hrl-lh-tight); }
|
|
241
241
|
/* The punch status stays in the accessibility tree (no `hidden`) so its first message is announced; empty, it takes no space. */
|
|
242
242
|
[data-hrl-geo-status]:empty { margin: 0; }
|
|
243
|
+
.hrl-geo-help { margin: var(--hrl-s-2) 0 0; padding-left: var(--hrl-s-4); color: var(--hrl-ink); }
|
|
244
|
+
.hrl-geo-help li + li { margin-top: var(--hrl-s-1); }
|
|
243
245
|
.hrl-grid { display: grid; gap: .85rem; grid-template-columns: 1fr; margin-bottom: .85rem; }
|
|
244
246
|
.hrl-grid > * { min-width: 0; }
|
|
245
247
|
.hrl-grid > .hrl-card { margin-bottom: 0; }
|
|
@@ -371,6 +373,23 @@ summary.hrl-btn::-webkit-details-marker { display: none; } /* Safari before 18.4
|
|
|
371
373
|
.hrl-feed__meta { color: var(--hrl-muted); font-size: var(--hrl-t-small); }
|
|
372
374
|
.hrl-feed__body { min-width: 0; flex: 1; overflow-wrap: anywhere; }
|
|
373
375
|
|
|
376
|
+
/* Employee timeline: a rail with a dot per event, grouped by month. Private
|
|
377
|
+
records sit on a shaded card with a dashed rail and hollow dots, and say
|
|
378
|
+
"Private" in words, so the difference never rests on colour alone. */
|
|
379
|
+
.hrl-timeline__month { margin: var(--hrl-s-4) 0 var(--hrl-s-2); font-size: var(--hrl-t-small); color: var(--hrl-muted); }
|
|
380
|
+
.hrl-timeline { list-style: none; margin: 0; padding: 0 0 0 var(--hrl-s-5); border-left: 2px solid var(--hrl-line); }
|
|
381
|
+
.hrl-timeline__item { position: relative; padding: 0 0 var(--hrl-s-4); min-width: 0; overflow-wrap: anywhere; }
|
|
382
|
+
.hrl-timeline__item::before {
|
|
383
|
+
content: ""; position: absolute; left: calc(-1 * var(--hrl-s-5) - 7px); top: 6px;
|
|
384
|
+
width: 12px; height: 12px; border-radius: var(--hrl-radius-pill); background: var(--hrl-accent);
|
|
385
|
+
}
|
|
386
|
+
.hrl-timeline__title { margin: var(--hrl-s-1) 0 0; font-weight: 600; }
|
|
387
|
+
.hrl-timeline__item p.hrl-feed__meta { margin: 2px 0 0; }
|
|
388
|
+
.hrl-card--private { background: var(--hrl-bg); border-style: dashed; }
|
|
389
|
+
.hrl-card--private .hrl-timeline { border-left-style: dashed; border-left-color: var(--hrl-field); }
|
|
390
|
+
.hrl-card--private .hrl-timeline__item::before { background: var(--hrl-bg); border: 2px solid var(--hrl-field); }
|
|
391
|
+
.hrl-card--private .hrl-badge--muted { background: var(--hrl-card); }
|
|
392
|
+
|
|
374
393
|
/* Mention autocomplete menu */
|
|
375
394
|
.hrl-mention-wrap { position: relative; }
|
|
376
395
|
.hrl-mention-menu {
|
|
@@ -401,6 +420,7 @@ summary.hrl-btn::-webkit-details-marker { display: none; } /* Safari before 18.4
|
|
|
401
420
|
.hrl-tree__card--me { border-color: var(--hrl-accent); background: var(--hrl-info-bg); }
|
|
402
421
|
.hrl-tree__name, .hrl-tree__role { min-width: 0; overflow-wrap: anywhere; }
|
|
403
422
|
.hrl-tree__name { font-weight: 700; font-size: var(--hrl-t-small); }
|
|
423
|
+
a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--hrl-target); }
|
|
404
424
|
.hrl-tree__role { color: var(--hrl-muted); font-size: var(--hrl-t-small); }
|
|
405
425
|
.hrl-chainrow { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
|
|
406
426
|
.hrl-chainrow__step { display: inline-flex; align-items: center; gap: .4rem; min-width: 0; }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
# Employee of the month, quarter and year. Leadership picks the winner.
|
|
4
|
+
class AwardsController < LeadershipController
|
|
5
|
+
before_action :set_award, only: %i[edit update]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@awards = paginate(Award.includes(:user).recent_first)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def new
|
|
12
|
+
@award = Award.new(kind: "month", period_start: Date.current)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create
|
|
16
|
+
@award = Award.new(award_params.merge(created_by_id: hr_current_user.id))
|
|
17
|
+
if @award.save
|
|
18
|
+
redirect_to admin_awards_path, notice: "#{@award.title} saved."
|
|
19
|
+
else
|
|
20
|
+
render :new, status: :unprocessable_entity
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def edit; end
|
|
25
|
+
|
|
26
|
+
def update
|
|
27
|
+
if @award.update(award_params)
|
|
28
|
+
redirect_to admin_awards_path, notice: "Award updated."
|
|
29
|
+
else
|
|
30
|
+
render :edit, status: :unprocessable_entity
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def set_award = @award = Award.find(params[:id])
|
|
37
|
+
|
|
38
|
+
def award_params
|
|
39
|
+
params.require(:award).permit(:user_id, :kind, :period_start, :citation)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
module Admin
|
|
3
|
+
# Performance improvement plans: the appraisal tier, so Super Admin only.
|
|
4
|
+
class PerformancePlansController < SuperadminController
|
|
5
|
+
before_action :set_plan, only: %i[edit update]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@plans = paginate(PerformancePlan.includes(:user).open_first)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def new
|
|
12
|
+
@plan = PerformancePlan.new(user_id: params[:user_id], start_date: Date.current, end_date: Date.current + 30)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create
|
|
16
|
+
@plan = PerformancePlan.new(plan_params.merge(created_by_id: hr_current_user.id))
|
|
17
|
+
if @plan.save
|
|
18
|
+
redirect_to admin_performance_plans_path, notice: "PIP started."
|
|
19
|
+
else
|
|
20
|
+
render :new, status: :unprocessable_entity
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def edit; end
|
|
25
|
+
|
|
26
|
+
def update
|
|
27
|
+
if @plan.update(plan_params.except(:user_id))
|
|
28
|
+
redirect_to admin_performance_plans_path, notice: "PIP updated."
|
|
29
|
+
else
|
|
30
|
+
render :edit, status: :unprocessable_entity
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def set_plan
|
|
37
|
+
@plan = PerformancePlan.find(params[:id])
|
|
38
|
+
redirect_to admin_performance_plans_path, alert: "A closed PIP cannot be changed." if @plan.closed?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def plan_params
|
|
42
|
+
params.require(:performance_plan).permit(:user_id, :start_date, :end_date, :goals, :status, :outcome_note)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -2,6 +2,7 @@ module HrLite
|
|
|
2
2
|
class HomeController < ApplicationController
|
|
3
3
|
def index
|
|
4
4
|
@latest_kudos = Kudo.recent.includes(:giver, kudo_mentions: :user).limit(3)
|
|
5
|
+
@awards = Award.current
|
|
5
6
|
@out_week = LeaveRequest.approved.overlapping_range(Date.current.beginning_of_week, Date.current.end_of_week)
|
|
6
7
|
.includes(:user).where(user_id: HrLite.active_employees.map(&:id))
|
|
7
8
|
.sort_by { |leave| hr_display_name(leave.user).downcase }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Anyone's year on one page. Every employee may open a colleague's timeline;
|
|
3
|
+
# HrLite::Timeline decides which events the viewer is allowed to see.
|
|
4
|
+
class TimelinesController < ApplicationController
|
|
5
|
+
def show
|
|
6
|
+
@person = params[:user_id] ? HrLite.employees.find { |u| u.id == params[:user_id].to_i } : hr_current_user
|
|
7
|
+
raise ActiveRecord::RecordNotFound unless @person
|
|
8
|
+
|
|
9
|
+
year = params[:year].to_i
|
|
10
|
+
@year = year.between?(2000, 2100) ? year : Date.current.year
|
|
11
|
+
@profile = EmployeeProfile.find_by(user_id: @person.id)
|
|
12
|
+
timeline = Timeline.new(user: @person, viewer: hr_current_user, year: @year)
|
|
13
|
+
@public_events = timeline.public_events
|
|
14
|
+
@private_events = timeline.private_events
|
|
15
|
+
# Shown only to someone who may see private rows at all: the person, or a reach-holder.
|
|
16
|
+
@private_card = @person.id == hr_current_user.id || @private_events.any?
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -17,6 +17,7 @@ module HrLite
|
|
|
17
17
|
{ label: "Documents", path: :documents_path, match: [ "/documents" ], group: "Me" },
|
|
18
18
|
{ label: "ID card", path: :id_card_path, match: [ "/id_card" ], group: "Me" },
|
|
19
19
|
{ label: "Career", path: :career_path, match: [ "/career", "/appraisals" ], group: "Me" },
|
|
20
|
+
{ label: "Timeline", path: :timeline_path, match: [ "/timeline" ], group: "Me" },
|
|
20
21
|
{ label: "Policies", path: :policies_path, match: [ "/policies" ], group: "Company" },
|
|
21
22
|
{ label: "Ask HR", path: :hr_requests_path, match: [ "/hr_requests" ], group: "Company" },
|
|
22
23
|
{ label: "Org", path: :org_path, match: [ "/org" ], group: "Company" },
|
|
@@ -42,6 +43,7 @@ module HrLite
|
|
|
42
43
|
|
|
43
44
|
LEADERSHIP_NAV_ITEMS = [
|
|
44
45
|
{ label: "Employees", path: :admin_employees_path, match: [ "/admin/employees" ] },
|
|
46
|
+
{ label: "Awards", path: :admin_awards_path, match: [ "/admin/awards" ] },
|
|
45
47
|
{ label: "Settings", path: :admin_leave_types_path, match: [ "/admin/leave_types", "/admin/office_locations", "/admin/holidays", "/admin/setting", "/admin/expense_categories" ] },
|
|
46
48
|
{ label: "Audit", path: :admin_audit_logs_path, match: [ "/admin/audit_logs" ] },
|
|
47
49
|
{ label: "Policies", path: :admin_policies_path, match: [ "/admin/policies" ] },
|
|
@@ -56,7 +58,8 @@ module HrLite
|
|
|
56
58
|
{ label: "Roles", path: :admin_roles_path, match: [ "/admin/roles" ] },
|
|
57
59
|
{ label: "Rates", path: :admin_statutory_rate_cards_path, match: [ "/admin/statutory_rate_cards" ] },
|
|
58
60
|
{ label: "Tax", path: :admin_tax_declarations_path, match: [ "/admin/tax_declarations" ] },
|
|
59
|
-
{ label: "Loans", path: :admin_loans_path, match: [ "/admin/loans" ] }
|
|
61
|
+
{ label: "Loans", path: :admin_loans_path, match: [ "/admin/loans" ] },
|
|
62
|
+
{ label: "PIPs", path: :admin_performance_plans_path, match: [ "/admin/performance_plans" ] }
|
|
60
63
|
].freeze
|
|
61
64
|
|
|
62
65
|
# DayStatus kind => [code, word, month-grid cell modifier]: the code keeps a state readable without its colour.
|
|
@@ -15,6 +15,7 @@ module HrLite
|
|
|
15
15
|
MONEY_TIER_TYPES = %w[
|
|
16
16
|
HrLite::Appraisal HrLite::DesignationChange HrLite::SalaryStructure
|
|
17
17
|
HrLite::PayrollRun HrLite::SalarySlip HrLite::PayrollLineItem HrLite::Loan
|
|
18
|
+
HrLite::PerformancePlan
|
|
18
19
|
].freeze
|
|
19
20
|
|
|
20
21
|
scope :recent, -> { order(created_at: :desc) }
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# Employee of the month, quarter or year. Leadership picks the winner; one
|
|
3
|
+
# per period, editable (to correct the winner) but never deleted.
|
|
4
|
+
class Award < ApplicationRecord
|
|
5
|
+
include Audited
|
|
6
|
+
|
|
7
|
+
KINDS = %w[month quarter year].freeze
|
|
8
|
+
|
|
9
|
+
belongs_to :user, class_name: HrLite.config.user_class
|
|
10
|
+
belongs_to :created_by, class_name: HrLite.config.user_class, optional: true
|
|
11
|
+
|
|
12
|
+
validates :kind, inclusion: { in: KINDS }
|
|
13
|
+
validates :period_start, :citation, presence: true
|
|
14
|
+
validates :period_start, uniqueness: { scope: :kind, message: "already has a winner" }
|
|
15
|
+
|
|
16
|
+
before_validation :normalise_period
|
|
17
|
+
after_save_commit :notify, if: -> { saved_change_to_user_id? }
|
|
18
|
+
|
|
19
|
+
scope :recent_first, -> { order(period_start: :desc, kind: :asc) }
|
|
20
|
+
|
|
21
|
+
# The latest started award of each kind, month first.
|
|
22
|
+
def self.current
|
|
23
|
+
KINDS.filter_map { |kind| where(kind: kind, period_start: ..Date.current).order(period_start: :desc).first }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def title = "Employee of the #{kind}"
|
|
27
|
+
|
|
28
|
+
def period_label
|
|
29
|
+
case kind
|
|
30
|
+
when "month" then period_start.strftime("%B %Y")
|
|
31
|
+
when "quarter" then "Q#{(period_start.month - 1) / 3 + 1} #{period_start.year}"
|
|
32
|
+
else period_start.year.to_s
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def normalise_period
|
|
39
|
+
return unless period_start
|
|
40
|
+
|
|
41
|
+
self.period_start = { "month" => period_start.beginning_of_month, "quarter" => period_start.beginning_of_quarter,
|
|
42
|
+
"year" => period_start.beginning_of_year }.fetch(kind, period_start)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def notify
|
|
46
|
+
Notifications.publish("award.won", title: "You are #{title} — #{period_label}", body: citation,
|
|
47
|
+
path: "/timeline", bell_to: [ user ], email_to: [ user ])
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# A performance improvement plan. Super Admin writes it (the appraisal
|
|
3
|
+
# tier); the employee is told when it starts and when it closes. Closing as
|
|
4
|
+
# passed or failed stamps closed_at and freezes the row.
|
|
5
|
+
class PerformancePlan < ApplicationRecord
|
|
6
|
+
include Audited
|
|
7
|
+
|
|
8
|
+
STATUSES = %w[active extended passed failed].freeze
|
|
9
|
+
CLOSED = %w[passed failed].freeze
|
|
10
|
+
|
|
11
|
+
belongs_to :user, class_name: HrLite.config.user_class
|
|
12
|
+
belongs_to :created_by, class_name: HrLite.config.user_class, optional: true
|
|
13
|
+
|
|
14
|
+
validates :start_date, :end_date, :goals, presence: true
|
|
15
|
+
validates :status, inclusion: { in: STATUSES }
|
|
16
|
+
validate :ends_after_start
|
|
17
|
+
|
|
18
|
+
scope :open_first, -> { order(Arel.sql("CASE WHEN status IN ('active', 'extended') THEN 0 ELSE 1 END"), :end_date) }
|
|
19
|
+
|
|
20
|
+
before_save { self.closed_at ||= Time.current if closed? }
|
|
21
|
+
after_create_commit { notify("pip.started", "Performance improvement plan started", "Runs until #{end_date.strftime('%d %b %Y')}.") }
|
|
22
|
+
after_update_commit do
|
|
23
|
+
notify("pip.closed", "Performance improvement plan #{status}", outcome_note) if saved_change_to_status? && closed?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def closed? = CLOSED.include?(status)
|
|
27
|
+
|
|
28
|
+
# A closed plan is the record of its outcome.
|
|
29
|
+
def readonly?
|
|
30
|
+
persisted? && CLOSED.include?(status_in_database) && !destroyed?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def ends_after_start
|
|
36
|
+
errors.add(:end_date, "must be after the start date") if start_date && end_date && end_date <= start_date
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def notify(event, title, body)
|
|
40
|
+
Notifications.publish(event, title: title, body: body.presence, path: "/timeline", bell_to: [ user ], email_to: [ user ])
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# One employee's year as `viewer` may see it, split into what everyone may
|
|
3
|
+
# read (#public_events) and what only the employee and whoever the matching
|
|
4
|
+
# permission reaches may read (#private_events). Leave type follows
|
|
5
|
+
# leave.view, like the "Out in the next 2 weeks" card.
|
|
6
|
+
class Timeline
|
|
7
|
+
Event = Struct.new(:date, :category, :title, :detail, keyword_init: true)
|
|
8
|
+
|
|
9
|
+
PAY_CODES = %w[bonus incentive arrears].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(user:, viewer:, year:)
|
|
12
|
+
@user = user
|
|
13
|
+
@viewer = viewer
|
|
14
|
+
@range = Date.new(year)..Date.new(year).end_of_year
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def public_events = newest_first(lifecycle + promotions + kudos_received + kudos_given + awards + leaves)
|
|
18
|
+
|
|
19
|
+
def private_events
|
|
20
|
+
newest_first(pay_items + loans + expenses + appraisals + plans + attendance_fixes + comp_offs +
|
|
21
|
+
assets + documents + resignations + probation + hr_requests)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def newest_first(events) = events.select { |e| @range.cover?(e.date) }.sort_by { |e| -e.date.jd }
|
|
27
|
+
|
|
28
|
+
def visible?(key)
|
|
29
|
+
@viewer.id == @user.id || HrLite.reaches?(@viewer, key, @user)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def mine = { user_id: @user.id }
|
|
33
|
+
def days = @range.begin.beginning_of_day..@range.end.end_of_day
|
|
34
|
+
def event(date, category, title, detail = nil) = Event.new(date: date, category: category, title: title, detail: detail.presence)
|
|
35
|
+
def profile = @profile ||= EmployeeProfile.find_by(user_id: @user.id)
|
|
36
|
+
|
|
37
|
+
# --- public ---------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def lifecycle
|
|
40
|
+
return [] unless profile
|
|
41
|
+
|
|
42
|
+
[ [ profile.date_of_joining, "Joined the company" ], [ profile.date_of_exit, "Left the company" ] ]
|
|
43
|
+
.select { |date, _| date && date <= Date.current }
|
|
44
|
+
.map { |date, title| event(date, "Milestone", title) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def promotions
|
|
48
|
+
DesignationChange.where(**mine, effective_date: @range.begin..[ @range.end, Date.current ].min).map do |change|
|
|
49
|
+
event(change.effective_date, "Promotion", "New role: #{change.to_designation}",
|
|
50
|
+
change.from_designation.presence && "from #{change.from_designation}")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def kudos_received
|
|
55
|
+
Kudo.joins(:kudo_mentions).where(hr_lite_kudo_mentions: mine, created_at: days).includes(:giver).map do |kudo|
|
|
56
|
+
event(kudo.created_at.to_date, "Kudos", "Kudos from #{HrLite.display_name(kudo.giver)}", kudo_text(kudo))
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def kudos_given
|
|
61
|
+
Kudo.where(giver_id: @user.id, created_at: days).includes(:mentioned_users).map do |kudo|
|
|
62
|
+
names = kudo.mentioned_users.map { |u| HrLite.display_name(u) }.to_sentence
|
|
63
|
+
event(kudo.created_at.to_date, "Kudos", "Gave kudos#{" to #{names}" if names.present?}", kudo_text(kudo))
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def kudo_text(kudo) = [ kudo.badge_label, MentionParser.strip_markers(kudo.message) ].compact.join(" · ")
|
|
68
|
+
|
|
69
|
+
def awards
|
|
70
|
+
Award.where(**mine, period_start: @range).map do |award|
|
|
71
|
+
event(award.period_start, "Award", "#{award.title} — #{award.period_label}", award.citation)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def leaves
|
|
76
|
+
typed = visible?("leave.view")
|
|
77
|
+
LeaveRequest.approved.where(mine).overlapping_range(@range.begin, @range.end).includes(:leave_type).map do |leave|
|
|
78
|
+
event(leave.start_date, "Leave", typed ? leave.leave_type.name : "On leave", leave.date_range_label)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# --- private --------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
def pay_items
|
|
85
|
+
return [] unless visible?("payroll.view")
|
|
86
|
+
|
|
87
|
+
PayrollLineItem.joins(:component).includes(:component).where(**mine, period_month: @range)
|
|
88
|
+
.where(hr_lite_salary_components: { code: PAY_CODES }).map do |item|
|
|
89
|
+
event(item.period_month, "Pay", item.component.label, [ Money.format(item.amount), item.note.presence ].compact.join(" · "))
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def loans
|
|
94
|
+
return [] unless visible?("payroll.view")
|
|
95
|
+
|
|
96
|
+
Loan.where(**mine, starts_on: @range).map { |loan| event(loan.starts_on, "Pay", "Loan of #{Money.format(loan.principal)}", loan.reason) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def expenses
|
|
100
|
+
return [] unless visible?("expense.approve")
|
|
101
|
+
|
|
102
|
+
Expense.where(**mine, status: %w[approved reimbursed], spent_on: @range).includes(:category).map do |expense|
|
|
103
|
+
event(expense.spent_on, "Expense", "Expense #{expense.status}: #{expense.category.name}",
|
|
104
|
+
"#{Money.format(expense.amount)} · #{expense.description}")
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def appraisals
|
|
109
|
+
return [] unless visible?("appraisal.view")
|
|
110
|
+
|
|
111
|
+
Appraisal.shared.where(**mine, shared_at: days).map do |appraisal|
|
|
112
|
+
outcome = appraisal.outcome == "none" ? nil : appraisal.outcome.humanize
|
|
113
|
+
event(appraisal.shared_at.to_date, "Performance", "Appraisal #{appraisal.period_label}",
|
|
114
|
+
[ appraisal.rating && "Rating #{appraisal.rating}/5", outcome ].compact.join(" · "))
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def plans
|
|
119
|
+
return [] unless visible?("appraisal.view")
|
|
120
|
+
|
|
121
|
+
PerformancePlan.where(mine).where(start_date: ..@range.end).flat_map do |plan|
|
|
122
|
+
[ event(plan.start_date, "Performance", "Performance improvement plan started", "Until #{plan.end_date.strftime('%d %b %Y')}"),
|
|
123
|
+
plan.closed_at && event(plan.closed_at.to_date, "Performance", "Performance improvement plan #{plan.status}", plan.outcome_note) ].compact
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def attendance_fixes
|
|
128
|
+
return [] unless visible?("attendance.view")
|
|
129
|
+
|
|
130
|
+
RegularizationRequest.where(**mine, status: "approved", date: @range).map do |fix|
|
|
131
|
+
event(fix.date, "Attendance", "Attendance fixed (#{fix.times_label})", fix.reason)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def comp_offs
|
|
136
|
+
return [] unless visible?("leave.view")
|
|
137
|
+
|
|
138
|
+
CompOffRequest.where(**mine, status: "approved", date_worked: @range).map do |comp_off|
|
|
139
|
+
event(comp_off.date_worked, "Leave", "Comp-off earned (#{comp_off.credit_days} day)", comp_off.reason)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def assets
|
|
144
|
+
return [] unless visible?("asset.view")
|
|
145
|
+
|
|
146
|
+
AssetAssignment.where(mine).includes(:asset).flat_map do |held|
|
|
147
|
+
[ event(held.assigned_on, "Asset", "Given #{held.asset.name}", held.asset.serial_number),
|
|
148
|
+
held.returned_on && event(held.returned_on, "Asset", "Returned #{held.asset.name}", held.condition_note) ].compact
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def documents
|
|
153
|
+
Document.where(**mine, verification: "verified", verified_at: days).select { |doc| doc.readable_by?(@viewer) }.map do |doc|
|
|
154
|
+
event(doc.verified_at.to_date, "Document", "Document verified: #{doc.title}")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def resignations
|
|
159
|
+
return [] unless visible?("resignation.view")
|
|
160
|
+
|
|
161
|
+
Resignation.where(mine).flat_map do |resignation|
|
|
162
|
+
accepted = resignation.status == "accepted" && resignation.decided_at
|
|
163
|
+
[ event(resignation.created_at.to_date, "Exit", "Resigned", "Proposed last day #{resignation.proposed_last_day.strftime('%d %b %Y')}"),
|
|
164
|
+
accepted && event(resignation.decided_at.to_date, "Exit", "Resignation accepted",
|
|
165
|
+
"Last day #{resignation.proposed_last_day.strftime('%d %b %Y')}") ].select(&:itself)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def probation
|
|
170
|
+
return [] unless profile&.probation_until && visible?("profile.view")
|
|
171
|
+
|
|
172
|
+
[ event(profile.probation_until, "Milestone", profile.probation_until < Date.current ? "Probation ended" : "Probation ends") ]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def hr_requests
|
|
176
|
+
return [] unless visible?("hr_request.manage")
|
|
177
|
+
|
|
178
|
+
HrRequest.where(**mine, status: %w[resolved closed], resolved_at: days).map do |request|
|
|
179
|
+
event(request.resolved_at.to_date, "Help desk", "Request resolved: #{request.subject}", request.resolution)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<section class="hrl-card">
|
|
2
|
+
<%= form_with model: award, url: url, method: method, scope: :award, local: true do |f| %>
|
|
3
|
+
<%= render "hr_lite/shared/form_errors", record: award %>
|
|
4
|
+
<div class="hrl-field">
|
|
5
|
+
<%= f.label :kind, "Award" %>
|
|
6
|
+
<%= f.select :kind, HrLite::Award::KINDS.map { |k| [ "Employee of the #{k}", k ] } %>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="hrl-field">
|
|
9
|
+
<%= f.label :period_start, "Any day in the period" %>
|
|
10
|
+
<%= f.date_field :period_start %>
|
|
11
|
+
<p class="hrl-hint">Saved as the start of that month, quarter or year.</p>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="hrl-field">
|
|
14
|
+
<%= f.label :user_id, "Winner" %>
|
|
15
|
+
<%= f.select :user_id, options_for_select(HrLite.active_employees.map { |u| [ hr_display_name(u), u.id ] }, award.user_id),
|
|
16
|
+
{ include_blank: "Choose…" }, data: { hrl_filter: "Type a name to filter" } %>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="hrl-field"><%= f.label :citation, "Why they won" %><%= f.text_area :citation, rows: 3 %></div>
|
|
19
|
+
<div class="hrl-form-actions">
|
|
20
|
+
<%= f.submit "Save", class: "hrl-btn hrl-btn--primary" %>
|
|
21
|
+
<a class="hrl-btn" href="<%= hr_lite.admin_awards_path %>">Cancel</a>
|
|
22
|
+
</div>
|
|
23
|
+
<% end %>
|
|
24
|
+
</section>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Awards" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Employee of the month, quarter and year</h1>
|
|
4
|
+
<div class="hrl-page__actions">
|
|
5
|
+
<a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_admin_award_path %>">Pick a winner</a>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<section class="hrl-card">
|
|
10
|
+
<% if @awards.any? %>
|
|
11
|
+
<div class="hrl-table-wrap">
|
|
12
|
+
<table class="hrl-table hrl-table--stack">
|
|
13
|
+
<thead><tr><th>Award</th><th>Period</th><th>Winner</th><th></th></tr></thead>
|
|
14
|
+
<tbody>
|
|
15
|
+
<% @awards.each do |award| %>
|
|
16
|
+
<tr>
|
|
17
|
+
<td data-label="Award"><%= award.title %></td>
|
|
18
|
+
<td data-label="Period"><%= award.period_label %></td>
|
|
19
|
+
<td data-label="Winner"><%= hr_display_name(award.user) %></td>
|
|
20
|
+
<td data-label=""><a class="hrl-card__link" href="<%= hr_lite.edit_admin_award_path(award) %>">Edit</a></td>
|
|
21
|
+
</tr>
|
|
22
|
+
<% end %>
|
|
23
|
+
</tbody>
|
|
24
|
+
</table>
|
|
25
|
+
</div>
|
|
26
|
+
<%= hrl_pagination %>
|
|
27
|
+
<% else %>
|
|
28
|
+
<p class="hrl-card--empty">No winners picked yet.</p>
|
|
29
|
+
<% end %>
|
|
30
|
+
</section>
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
<div class="hrl-page__actions">
|
|
5
5
|
<a class="hrl-btn" href="<%= hr_lite.edit_admin_employee_path(@profile) %>">Edit</a>
|
|
6
6
|
<a class="hrl-btn" href="<%= hr_lite.id_card_path(user_id: @profile.user_id) %>">ID card</a>
|
|
7
|
+
<a class="hrl-btn" href="<%= hr_lite.person_timeline_path(@profile.user_id) %>">Timeline</a>
|
|
7
8
|
<% if hr_superadmin? %>
|
|
8
9
|
<a class="hrl-btn" href="<%= hr_lite.new_admin_employee_appraisal_path(@profile) %>">New appraisal</a>
|
|
9
10
|
<a class="hrl-btn" href="<%= hr_lite.new_admin_employee_designation_change_path(@profile) %>">Role change</a>
|
|
11
|
+
<a class="hrl-btn" href="<%= hr_lite.new_admin_performance_plan_path(user_id: @profile.user_id) %>">Start PIP</a>
|
|
10
12
|
<a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_admin_employee_salary_structure_path(@profile) %>">New structure</a>
|
|
11
13
|
<% end %>
|
|
12
14
|
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<section class="hrl-card">
|
|
2
|
+
<%= form_with model: plan, url: url, method: method, scope: :performance_plan, local: true do |f| %>
|
|
3
|
+
<%= render "hr_lite/shared/form_errors", record: plan %>
|
|
4
|
+
<div class="hrl-field">
|
|
5
|
+
<%= f.label :user_id, "Employee" %>
|
|
6
|
+
<% if plan.persisted? %>
|
|
7
|
+
<p><strong><%= hr_display_name(plan.user) %></strong></p>
|
|
8
|
+
<% else %>
|
|
9
|
+
<%= f.select :user_id, options_for_select(HrLite.active_employees.map { |u| [ hr_display_name(u), u.id ] }, plan.user_id),
|
|
10
|
+
{ include_blank: "Choose…" }, data: { hrl_filter: "Type a name to filter" } %>
|
|
11
|
+
<% end %>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="hrl-field"><%= f.label :start_date %><%= f.date_field :start_date %></div>
|
|
14
|
+
<div class="hrl-field"><%= f.label :end_date, "Review by" %><%= f.date_field :end_date %></div>
|
|
15
|
+
<div class="hrl-field"><%= f.label :goals, "Goals the employee must meet" %><%= f.text_area :goals, rows: 5 %></div>
|
|
16
|
+
<% if plan.persisted? %>
|
|
17
|
+
<div class="hrl-field">
|
|
18
|
+
<%= f.label :status %>
|
|
19
|
+
<%= f.select :status, HrLite::PerformancePlan::STATUSES.map { |s| [ s.humanize, s ] } %>
|
|
20
|
+
<p class="hrl-hint">Passed or failed closes the plan for good and tells the employee.</p>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="hrl-field"><%= f.label :outcome_note, "Outcome note" %><%= f.text_area :outcome_note, rows: 3 %></div>
|
|
23
|
+
<% end %>
|
|
24
|
+
<div class="hrl-form-actions">
|
|
25
|
+
<%= f.submit plan.persisted? ? "Save" : "Start PIP", class: "hrl-btn hrl-btn--primary" %>
|
|
26
|
+
<a class="hrl-btn" href="<%= hr_lite.admin_performance_plans_path %>">Cancel</a>
|
|
27
|
+
</div>
|
|
28
|
+
<% end %>
|
|
29
|
+
</section>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<% content_for(:page_title) { "PIP — #{hr_display_name(@plan.user)}" } %>
|
|
2
|
+
<div class="hrl-page__head"><h1 class="hrl-page__title">PIP — <%= hr_display_name(@plan.user) %></h1></div>
|
|
3
|
+
<%= render "form", plan: @plan, url: hr_lite.admin_performance_plan_path(@plan), method: :patch %>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<% content_for(:page_title) { "PIPs" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Performance improvement plans</h1>
|
|
4
|
+
<div class="hrl-page__actions">
|
|
5
|
+
<a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_admin_performance_plan_path %>">Start a PIP</a>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<section class="hrl-card">
|
|
10
|
+
<% if @plans.any? %>
|
|
11
|
+
<div class="hrl-table-wrap">
|
|
12
|
+
<table class="hrl-table hrl-table--stack">
|
|
13
|
+
<thead><tr><th>Employee</th><th>Period</th><th>Status</th><th></th></tr></thead>
|
|
14
|
+
<tbody>
|
|
15
|
+
<% @plans.each do |plan| %>
|
|
16
|
+
<tr>
|
|
17
|
+
<td data-label="Employee"><a class="hrl-card__link" href="<%= hr_lite.person_timeline_path(plan.user_id) %>"><%= hr_display_name(plan.user) %></a></td>
|
|
18
|
+
<td data-label="Period"><%= plan.start_date.strftime("%-d %b %Y") %> – <%= plan.end_date.strftime("%-d %b %Y") %></td>
|
|
19
|
+
<td data-label="Status"><span class="hrl-badge <%= { "passed" => "hrl-badge--ok", "failed" => "hrl-badge--bad" }.fetch(plan.status, "hrl-badge--warn") %>"><%= plan.status.humanize %></span></td>
|
|
20
|
+
<td data-label=""><% unless plan.closed? %><a class="hrl-card__link" href="<%= hr_lite.edit_admin_performance_plan_path(plan) %>">Update</a><% end %></td>
|
|
21
|
+
</tr>
|
|
22
|
+
<% end %>
|
|
23
|
+
</tbody>
|
|
24
|
+
</table>
|
|
25
|
+
</div>
|
|
26
|
+
<%= hrl_pagination %>
|
|
27
|
+
<% else %>
|
|
28
|
+
<p class="hrl-card--empty">No PIPs yet.</p>
|
|
29
|
+
<% end %>
|
|
30
|
+
</section>
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
<% if open_yesterday %>
|
|
36
36
|
<%= form_with url: hr_lite.check_out_attendance_path, method: :post, local: true,
|
|
37
|
-
html: { data: { "hrl-geo-punch": true } } do %>
|
|
37
|
+
html: { data: { "hrl-geo-punch": true, "hrl-geo-required": HrLite.config.require_punch_location.presence } } do %>
|
|
38
38
|
<input type="hidden" name="lat"><input type="hidden" name="lng">
|
|
39
39
|
<input type="hidden" name="accuracy_m"><input type="hidden" name="geo_status">
|
|
40
40
|
<button type="submit" class="hrl-btn hrl-btn--big hrl-btn--block">
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
<% end %>
|
|
45
45
|
<% elsif record&.check_in_at.blank? %>
|
|
46
46
|
<%= form_with url: hr_lite.check_in_attendance_path, method: :post, local: true,
|
|
47
|
-
html: { data: { "hrl-geo-punch": true } } do %>
|
|
47
|
+
html: { data: { "hrl-geo-punch": true, "hrl-geo-required": HrLite.config.require_punch_location.presence } } do %>
|
|
48
48
|
<input type="hidden" name="lat"><input type="hidden" name="lng">
|
|
49
49
|
<input type="hidden" name="accuracy_m"><input type="hidden" name="geo_status">
|
|
50
50
|
<button type="submit" class="hrl-btn hrl-btn--primary hrl-btn--big hrl-btn--block">Check in</button>
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
<% end %>
|
|
53
53
|
<% else %>
|
|
54
54
|
<%= form_with url: hr_lite.check_out_attendance_path, method: :post, local: true,
|
|
55
|
-
html: { data: { "hrl-geo-punch": true } } do %>
|
|
55
|
+
html: { data: { "hrl-geo-punch": true, "hrl-geo-required": HrLite.config.require_punch_location.presence } } do %>
|
|
56
56
|
<input type="hidden" name="lat"><input type="hidden" name="lng">
|
|
57
57
|
<input type="hidden" name="accuracy_m"><input type="hidden" name="geo_status">
|
|
58
58
|
<button type="submit" class="hrl-btn hrl-btn--big hrl-btn--block">
|
|
@@ -38,6 +38,25 @@
|
|
|
38
38
|
<a class="hrl-card__link" href="<%= hr_lite.team_path %>">Team board</a>
|
|
39
39
|
</section>
|
|
40
40
|
|
|
41
|
+
<% if @awards.any? %>
|
|
42
|
+
<section class="hrl-card">
|
|
43
|
+
<h2 class="hrl-card__title">Employee of the month, quarter and year</h2>
|
|
44
|
+
<ul class="hrl-feed">
|
|
45
|
+
<% @awards.each do |award| %>
|
|
46
|
+
<li class="hrl-feed__item">
|
|
47
|
+
<div class="hrl-feed__body">
|
|
48
|
+
<div class="hrl-row">
|
|
49
|
+
<a class="hrl-card__link" href="<%= hr_lite.person_timeline_path(award.user_id) %>"><%= hr_display_name(award.user) %></a>
|
|
50
|
+
<span class="hrl-badge"><%= award.title %> · <%= award.period_label %></span>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="hrl-feed__meta"><%= award.citation %></div>
|
|
53
|
+
</div>
|
|
54
|
+
</li>
|
|
55
|
+
<% end %>
|
|
56
|
+
</ul>
|
|
57
|
+
</section>
|
|
58
|
+
<% end %>
|
|
59
|
+
|
|
41
60
|
<section class="hrl-card">
|
|
42
61
|
<h2 class="hrl-card__title">Latest kudos</h2>
|
|
43
62
|
<% if @latest_kudos.any? %>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%# locals: profile — recursive org-chart node; @children / hr_current_user from OrgController %>
|
|
2
2
|
<li class="hrl-tree__node">
|
|
3
3
|
<div class="hrl-tree__card <%= 'hrl-tree__card--me' if profile.user_id == hr_current_user.id %>">
|
|
4
|
-
<
|
|
4
|
+
<a class="hrl-tree__name" href="<%= hr_lite.person_timeline_path(profile.user_id) %>"><%= hr_display_name(profile.user) %></a>
|
|
5
5
|
<% if profile.designation.present? %><span class="hrl-tree__role"><%= profile.designation %></span><% end %>
|
|
6
6
|
<% if profile.department.present? %><span class="hrl-small hrl-muted"><%= profile.department %></span><% end %>
|
|
7
7
|
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<%# locals: events, empty — one rail, grouped by month, newest first %>
|
|
2
|
+
<% if events.any? %>
|
|
3
|
+
<% events.group_by { |e| e.date.beginning_of_month }.each do |month, rows| %>
|
|
4
|
+
<h3 class="hrl-timeline__month"><%= month.strftime("%B %Y") %></h3>
|
|
5
|
+
<ol class="hrl-timeline">
|
|
6
|
+
<% rows.each do |event| %>
|
|
7
|
+
<li class="hrl-timeline__item">
|
|
8
|
+
<div class="hrl-row">
|
|
9
|
+
<span class="hrl-badge hrl-badge--muted"><%= event.category %></span>
|
|
10
|
+
<time class="hrl-feed__meta hrl-right" datetime="<%= event.date.iso8601 %>"><%= event.date.strftime("%a %-d %b") %></time>
|
|
11
|
+
</div>
|
|
12
|
+
<p class="hrl-timeline__title"><%= event.title %></p>
|
|
13
|
+
<% if event.detail %><p class="hrl-feed__meta"><%= event.detail %></p><% end %>
|
|
14
|
+
</li>
|
|
15
|
+
<% end %>
|
|
16
|
+
</ol>
|
|
17
|
+
<% end %>
|
|
18
|
+
<% else %>
|
|
19
|
+
<p class="hrl-muted hrl-small"><%= empty %></p>
|
|
20
|
+
<% end %>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Timeline — #{hr_display_name(@person)}" } %>
|
|
2
|
+
<% year_path = ->(y) { @person.id == hr_current_user.id ? hr_lite.timeline_path(year: y) : hr_lite.person_timeline_path(@person.id, year: y) } %>
|
|
3
|
+
<% name = hr_display_name(@person) %>
|
|
4
|
+
<div class="hrl-page__head">
|
|
5
|
+
<h1 class="hrl-page__title"><%= name %></h1>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<section class="hrl-card">
|
|
9
|
+
<dl class="hrl-deflist">
|
|
10
|
+
<% if @profile&.designation.present? %><dt>Role</dt><dd><%= @profile.designation %></dd><% end %>
|
|
11
|
+
<% if @profile&.department.present? %><dt>Department</dt><dd><%= @profile.department %></dd><% end %>
|
|
12
|
+
<% if @profile %>
|
|
13
|
+
<dt>Joined</dt><dd><%= @profile.date_of_joining.strftime("%-d %B %Y") %>
|
|
14
|
+
<% unless @profile.date_of_exit && @profile.date_of_exit <= Date.current %><span class="hrl-muted">· <%= distance_of_time_in_words(@profile.date_of_joining, Date.current) %></span><% end %></dd>
|
|
15
|
+
<% end %>
|
|
16
|
+
</dl>
|
|
17
|
+
<div class="hrl-row hrl-mt">
|
|
18
|
+
<a class="hrl-btn" href="<%= year_path.(@year - 1) %>">← <%= @year - 1 %></a>
|
|
19
|
+
<strong><%= @year %></strong>
|
|
20
|
+
<a class="hrl-btn" href="<%= year_path.(@year + 1) %>"><%= @year + 1 %> →</a>
|
|
21
|
+
</div>
|
|
22
|
+
</section>
|
|
23
|
+
|
|
24
|
+
<section class="hrl-card">
|
|
25
|
+
<h2 class="hrl-card__title">Public timeline</h2>
|
|
26
|
+
<p class="hrl-small hrl-muted">Everyone at the company can see this.</p>
|
|
27
|
+
<%= render "events", events: @public_events, empty: "Nothing public in #{@year}." %>
|
|
28
|
+
</section>
|
|
29
|
+
|
|
30
|
+
<% if @private_card %>
|
|
31
|
+
<section class="hrl-card hrl-card--private" aria-labelledby="private-records">
|
|
32
|
+
<h2 class="hrl-card__title" id="private-records">Private records</h2>
|
|
33
|
+
<p class="hrl-small"><strong>Private</strong> — only <%= @person.id == hr_current_user.id ? "you" : name %> and people with access can see this.</p>
|
|
34
|
+
<%= render "events", events: @private_events, empty: "Nothing private in #{@year}." %>
|
|
35
|
+
</section>
|
|
36
|
+
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -39,6 +39,8 @@ HrLite::Engine.routes.draw do
|
|
|
39
39
|
resources :loans, only: :index
|
|
40
40
|
get "team", to: "team#show"
|
|
41
41
|
get "org", to: "org#show"
|
|
42
|
+
get "timeline", to: "timelines#show"
|
|
43
|
+
get "people/:user_id/timeline", to: "timelines#show", as: :person_timeline
|
|
42
44
|
resources :holidays, only: :index
|
|
43
45
|
get "calendar", to: "calendar#show"
|
|
44
46
|
resources :salary_slips, only: %i[index show]
|
|
@@ -116,6 +118,8 @@ HrLite::Engine.routes.draw do
|
|
|
116
118
|
end
|
|
117
119
|
resources :designation_changes, only: %i[new create]
|
|
118
120
|
end
|
|
121
|
+
resources :performance_plans, only: %i[index new create edit update]
|
|
122
|
+
resources :awards, only: %i[index new create edit update]
|
|
119
123
|
resources :payroll_runs, only: %i[index show new create destroy] do
|
|
120
124
|
member do
|
|
121
125
|
post :compute
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class CreateHrLitePerformancePlans < ActiveRecord::Migration[8.1]
|
|
2
|
+
# A performance improvement plan: goals with a deadline, closed as passed or
|
|
3
|
+
# failed. Money tier, like appraisals — never deleted.
|
|
4
|
+
def change
|
|
5
|
+
create_table :hr_lite_performance_plans do |t|
|
|
6
|
+
t.bigint :user_id, null: false
|
|
7
|
+
t.date :start_date, null: false
|
|
8
|
+
t.date :end_date, null: false
|
|
9
|
+
t.text :goals, null: false
|
|
10
|
+
# active | extended | passed | failed
|
|
11
|
+
t.string :status, null: false, default: "active"
|
|
12
|
+
t.text :outcome_note
|
|
13
|
+
t.datetime :closed_at
|
|
14
|
+
t.bigint :created_by_id
|
|
15
|
+
t.timestamps
|
|
16
|
+
end
|
|
17
|
+
add_index :hr_lite_performance_plans, :user_id
|
|
18
|
+
add_check_constraint :hr_lite_performance_plans,
|
|
19
|
+
"status IN ('active', 'extended', 'passed', 'failed')",
|
|
20
|
+
name: "hr_lite_performance_plans_status_check"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class CreateHrLiteAwards < ActiveRecord::Migration[8.1]
|
|
2
|
+
# Employee of the month, quarter and year: one winner per period.
|
|
3
|
+
def change
|
|
4
|
+
create_table :hr_lite_awards do |t|
|
|
5
|
+
t.bigint :user_id, null: false
|
|
6
|
+
# month | quarter | year
|
|
7
|
+
t.string :kind, null: false
|
|
8
|
+
t.date :period_start, null: false
|
|
9
|
+
t.text :citation, null: false
|
|
10
|
+
t.bigint :created_by_id
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
add_index :hr_lite_awards, %i[kind period_start], unique: true
|
|
14
|
+
add_index :hr_lite_awards, :user_id
|
|
15
|
+
add_check_constraint :hr_lite_awards, "kind IN ('month', 'quarter', 'year')", name: "hr_lite_awards_kind_check"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -26,6 +26,9 @@ module HrLite
|
|
|
26
26
|
"kudos.mentioned" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
27
27
|
"appraisal.shared" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
|
|
28
28
|
"promotion.recorded" => { bell: true, email: true, leadership_email: true, leadership_bell: true },
|
|
29
|
+
"pip.started" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
30
|
+
"pip.closed" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
31
|
+
"award.won" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
|
|
29
32
|
"policy.changed" => { bell: false, email: false, leadership_email: true, leadership_bell: true },
|
|
30
33
|
# An approval past its deadline. Leadership hears too — a decision
|
|
31
34
|
# nobody is making is a process problem, not just one person's inbox.
|
data/lib/hr_lite/version.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.23.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kshitiz sinha
|
|
@@ -76,6 +76,7 @@ files:
|
|
|
76
76
|
- app/controllers/hr_lite/admin/assets_controller.rb
|
|
77
77
|
- app/controllers/hr_lite/admin/attendances_controller.rb
|
|
78
78
|
- app/controllers/hr_lite/admin/audit_logs_controller.rb
|
|
79
|
+
- app/controllers/hr_lite/admin/awards_controller.rb
|
|
79
80
|
- app/controllers/hr_lite/admin/base_controller.rb
|
|
80
81
|
- app/controllers/hr_lite/admin/benefits_controller.rb
|
|
81
82
|
- app/controllers/hr_lite/admin/checklists_controller.rb
|
|
@@ -96,6 +97,7 @@ files:
|
|
|
96
97
|
- app/controllers/hr_lite/admin/overview_controller.rb
|
|
97
98
|
- app/controllers/hr_lite/admin/payroll_line_items_controller.rb
|
|
98
99
|
- app/controllers/hr_lite/admin/payroll_runs_controller.rb
|
|
100
|
+
- app/controllers/hr_lite/admin/performance_plans_controller.rb
|
|
99
101
|
- app/controllers/hr_lite/admin/policies_controller.rb
|
|
100
102
|
- app/controllers/hr_lite/admin/regularization_requests_controller.rb
|
|
101
103
|
- app/controllers/hr_lite/admin/reports_controller.rb
|
|
@@ -134,6 +136,7 @@ files:
|
|
|
134
136
|
- app/controllers/hr_lite/salary_slips_controller.rb
|
|
135
137
|
- app/controllers/hr_lite/tax_declarations_controller.rb
|
|
136
138
|
- app/controllers/hr_lite/team_controller.rb
|
|
139
|
+
- app/controllers/hr_lite/timelines_controller.rb
|
|
137
140
|
- app/controllers/hr_lite/users_controller.rb
|
|
138
141
|
- app/helpers/hr_lite/application_helper.rb
|
|
139
142
|
- app/jobs/hr_lite/application_job.rb
|
|
@@ -159,6 +162,7 @@ files:
|
|
|
159
162
|
- app/models/hr_lite/asset_assignment.rb
|
|
160
163
|
- app/models/hr_lite/attendance_record.rb
|
|
161
164
|
- app/models/hr_lite/audit_log.rb
|
|
165
|
+
- app/models/hr_lite/award.rb
|
|
162
166
|
- app/models/hr_lite/benefit.rb
|
|
163
167
|
- app/models/hr_lite/benefit_enrolment.rb
|
|
164
168
|
- app/models/hr_lite/checklist_item.rb
|
|
@@ -181,6 +185,7 @@ files:
|
|
|
181
185
|
- app/models/hr_lite/office_location.rb
|
|
182
186
|
- app/models/hr_lite/payroll_line_item.rb
|
|
183
187
|
- app/models/hr_lite/payroll_run.rb
|
|
188
|
+
- app/models/hr_lite/performance_plan.rb
|
|
184
189
|
- app/models/hr_lite/policy.rb
|
|
185
190
|
- app/models/hr_lite/policy_acknowledgement.rb
|
|
186
191
|
- app/models/hr_lite/professional_tax_slab.rb
|
|
@@ -212,6 +217,7 @@ files:
|
|
|
212
217
|
- app/services/hr_lite/pdf_renderer.rb
|
|
213
218
|
- app/services/hr_lite/slip_builder.rb
|
|
214
219
|
- app/services/hr_lite/team_day.rb
|
|
220
|
+
- app/services/hr_lite/timeline.rb
|
|
215
221
|
- app/services/hr_lite/working_calendar.rb
|
|
216
222
|
- app/views/hr_lite/admin/appraisals/_form.html.erb
|
|
217
223
|
- app/views/hr_lite/admin/appraisals/edit.html.erb
|
|
@@ -222,6 +228,10 @@ files:
|
|
|
222
228
|
- app/views/hr_lite/admin/attendances/show.html.erb
|
|
223
229
|
- app/views/hr_lite/admin/attendances/week.html.erb
|
|
224
230
|
- app/views/hr_lite/admin/audit_logs/index.html.erb
|
|
231
|
+
- app/views/hr_lite/admin/awards/_form.html.erb
|
|
232
|
+
- app/views/hr_lite/admin/awards/edit.html.erb
|
|
233
|
+
- app/views/hr_lite/admin/awards/index.html.erb
|
|
234
|
+
- app/views/hr_lite/admin/awards/new.html.erb
|
|
225
235
|
- app/views/hr_lite/admin/benefits/index.html.erb
|
|
226
236
|
- app/views/hr_lite/admin/benefits/new.html.erb
|
|
227
237
|
- app/views/hr_lite/admin/checklists/index.html.erb
|
|
@@ -264,6 +274,10 @@ files:
|
|
|
264
274
|
- app/views/hr_lite/admin/payroll_runs/index.html.erb
|
|
265
275
|
- app/views/hr_lite/admin/payroll_runs/new.html.erb
|
|
266
276
|
- app/views/hr_lite/admin/payroll_runs/show.html.erb
|
|
277
|
+
- app/views/hr_lite/admin/performance_plans/_form.html.erb
|
|
278
|
+
- app/views/hr_lite/admin/performance_plans/edit.html.erb
|
|
279
|
+
- app/views/hr_lite/admin/performance_plans/index.html.erb
|
|
280
|
+
- app/views/hr_lite/admin/performance_plans/new.html.erb
|
|
267
281
|
- app/views/hr_lite/admin/policies/index.html.erb
|
|
268
282
|
- app/views/hr_lite/admin/policies/new.html.erb
|
|
269
283
|
- app/views/hr_lite/admin/regularization_requests/index.html.erb
|
|
@@ -337,6 +351,8 @@ files:
|
|
|
337
351
|
- app/views/hr_lite/shared/_pagination.html.erb
|
|
338
352
|
- app/views/hr_lite/tax_declarations/show.html.erb
|
|
339
353
|
- app/views/hr_lite/team/show.html.erb
|
|
354
|
+
- app/views/hr_lite/timelines/_events.html.erb
|
|
355
|
+
- app/views/hr_lite/timelines/show.html.erb
|
|
340
356
|
- app/views/layouts/hr_lite/application.html.erb
|
|
341
357
|
- app/views/layouts/hr_lite/mailer.html.erb
|
|
342
358
|
- app/views/layouts/hr_lite/mailer.text.erb
|
|
@@ -382,6 +398,8 @@ files:
|
|
|
382
398
|
- db/migrate/20261006090000_add_probation_until_to_hr_lite_employee_profiles.rb
|
|
383
399
|
- db/migrate/20261006120000_add_half_day_part_to_hr_lite_leave_requests.rb
|
|
384
400
|
- db/migrate/20261006120100_add_half_day_part_to_hr_lite_attendance_records.rb
|
|
401
|
+
- db/migrate/20261010090000_create_hr_lite_performance_plans.rb
|
|
402
|
+
- db/migrate/20261010090100_create_hr_lite_awards.rb
|
|
385
403
|
- lib/generators/hr_lite/install/install_generator.rb
|
|
386
404
|
- lib/generators/hr_lite/install/templates/AFTER_INSTALL
|
|
387
405
|
- lib/generators/hr_lite/install/templates/initializer.rb
|