hr_lite 0.37.1 → 0.40.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/app/assets/javascripts/hr_lite/nav.js +5 -0
- data/app/assets/stylesheets/hr_lite/hr_lite.css +21 -14
- data/app/controllers/hr_lite/admin/base_controller.rb +1 -5
- data/app/controllers/hr_lite/application_controller.rb +7 -1
- data/app/helpers/hr_lite/application_helper.rb +152 -72
- data/app/views/hr_lite/admin/attendances/show.html.erb +1 -0
- data/app/views/hr_lite/admin/comp_off_requests/index.html.erb +0 -2
- data/app/views/hr_lite/admin/documents/show.html.erb +1 -0
- data/app/views/hr_lite/admin/employees/show.html.erb +16 -9
- data/app/views/hr_lite/admin/leave_balances/index.html.erb +0 -2
- data/app/views/hr_lite/admin/leave_requests/index.html.erb +0 -2
- data/app/views/hr_lite/admin/regularization_requests/index.html.erb +0 -2
- data/app/views/hr_lite/admin/tax_declarations/person.html.erb +1 -0
- data/app/views/hr_lite/admin/wfh_requests/index.html.erb +0 -2
- data/app/views/hr_lite/admin/work_locations/index.html.erb +0 -2
- data/app/views/hr_lite/admin/work_time/index.html.erb +1 -0
- data/app/views/hr_lite/approvals/index.html.erb +11 -1
- data/app/views/hr_lite/comp_off_requests/index.html.erb +1 -1
- data/app/views/hr_lite/comp_off_requests/new.html.erb +1 -1
- data/app/views/hr_lite/id_cards/show.html.erb +3 -1
- data/app/views/hr_lite/leave_balances/index.html.erb +1 -1
- data/app/views/hr_lite/leave_requests/new.html.erb +1 -1
- data/app/views/hr_lite/leave_requests/show.html.erb +1 -1
- data/app/views/hr_lite/shared/_person_tabs.html.erb +21 -0
- data/app/views/hr_lite/timelines/show.html.erb +1 -0
- data/app/views/hr_lite/work_time/_timeline.html.erb +1 -1
- data/app/views/layouts/hr_lite/application.html.erb +21 -59
- data/lib/hr_lite/version.rb +1 -1
- metadata +2 -2
- data/app/views/hr_lite/admin/shared/_approvals_tabs.html.erb +0 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: beaf157b1f52eee0cddeed95c889cf3b27712630941baacd5b60fbfd1ba2b481
|
|
4
|
+
data.tar.gz: 8a22145f2f009a31103bbe27f63e3d1418d0c051789bf0922e12a3ef9bc5e119
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e7aa940b9bd69ecf36183e72b9188923be608d9304abe86a9cbb676d55a8235a77505f55a581e6e8ab4516fa3f24460915ad26ce11e3693a1b31f551f875b034
|
|
7
|
+
data.tar.gz: 0e1ae948beda575e12fe9ad9b01608eac52bc4ea276d0e7a2292ce6a71b39d14e126af1248cd78de389bd696abef87a496ae3e60930e47e107dd99cf20c2b7a1
|
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.40.2] - 2026-10-15
|
|
11
|
+
|
|
12
|
+
- ID card: a long name no longer runs off the card. When someone has no name and their email stands in for it ("sageashish2714@gmail.com"), it breaks before the "@" onto a second line. Any other unbroken word breaks wherever it has to.
|
|
13
|
+
|
|
14
|
+
## [0.40.1] - 2026-10-15
|
|
15
|
+
|
|
16
|
+
- Managers see their team's tabs. A Manager (leave and attendance at team scope) now gets Home › Company overview, Attendance › Team and Work modes, Leave › Team balances, Inbox (All, Leave requests, Comp-off, Attendance fixes, WFH, Home locations) and Admin › Reports. Those pages already let a manager in and show only their team's rows. The menu hid them unless you had the permission across the whole company or were leadership.
|
|
17
|
+
- These tabs now use the same check as their page (`hr_operations?`, which `Admin::BaseController` also uses), so they can't drift apart. Attendance › Team work time drops the expense-approver rule, because its page never admitted expense approvers. Admin › Reports shows for anyone who can open at least one report, which includes Finance.
|
|
18
|
+
- A manager's Inbox count and All list hold only their team's requests: each queue is scoped by the viewer's reach (0.40.0).
|
|
19
|
+
- The person tab row (0.39.0) reads `hr_operations?` too, instead of repeating its three permission checks.
|
|
20
|
+
|
|
21
|
+
## [0.40.0] - 2026-10-15
|
|
22
|
+
|
|
23
|
+
- Inbox › All lists everything waiting on you in one place: approval-flow steps, plus every leave request, comp-off, attendance fix, WFH request, home location, claim and document waiting in the queues you can decide. Rows run oldest first. Each row has an Open button that goes to that queue's own page, where you decide as before. Inline Approve/Reject is not built yet.
|
|
24
|
+
- Inbox shows how many items are waiting, on the menu and on each queue tab. On the phone the count sits on the icon's corner, so the bar keeps its height. Before, the queue tabs counted every pending request in the company. Now they count only what you can decide (your reach, never your own requests), the same scope each queue page uses to decide.
|
|
25
|
+
- All opens Inbox whenever you can see any queue, not only when an approval flow is active. Someone with nothing to decide still has no Inbox.
|
|
26
|
+
- Each Inbox tab's `badge:` is replaced by `queue:`, a scoped relation that drives both the counts and the All list.
|
|
27
|
+
|
|
28
|
+
## [0.39.0] - 2026-10-15
|
|
29
|
+
|
|
30
|
+
- One person's records are tabs: Overview, Attendance, Work time, Documents, Tax, Timeline, ID card. The row replaces the page's usual tab row on the seven per-person pages (the employee page, their attendance month, work time, documents, tax, timeline and ID card), so HR steps through someone's records without picking them again.
|
|
31
|
+
- Each tab shows only when its page would open for the viewer, using the same check that page makes. A manager sees their own report's Attendance, Timeline and ID card. A colleague sees no record tabs, and the timeline keeps the People tabs. A row with a single tab is not drawn.
|
|
32
|
+
- Employee page: the Edit button stays. New appraisal, Role change, Start PIP and New structure fold into one "Manage" button. The ID card, Timeline, Work time, Attendance and Documents buttons are now tabs.
|
|
33
|
+
- Layout: a page can fill the tab row slot itself with `content_for(:tabrow)`.
|
|
34
|
+
|
|
35
|
+
## [0.38.1] - 2026-10-15
|
|
36
|
+
|
|
37
|
+
- Each of the nine places has a stroke icon in the rail, the phone bar and the More sheet. Icons are `aria-hidden`, and the label always shows beside the icon (in the phone bar, under it). Tabs carry no icons.
|
|
38
|
+
- The More sheet is one column. Under each place it lists that person's tabs in it ("Profile, Documents, ID card, Career, Timeline"), on one line that ends in "…" when it runs long.
|
|
39
|
+
- The rail spaces its links 8 px apart (was 2 px), now that it holds nine items rather than about forty.
|
|
40
|
+
|
|
41
|
+
## [0.38.0] - 2026-10-15
|
|
42
|
+
|
|
43
|
+
- The menu has nine places instead of about 40 links in eight groups: Home, Attendance, Leave, Inbox, Pay, People, Me, Help, Admin. Each place has a row of tabs at the top of its pages. Your own tabs and your team's sit side by side, so "My attendance", "My work time", "Team" and "Team work time" are neighbours. Each place opens on its first tab, and a place shows only when at least one of its tabs does.
|
|
44
|
+
- No URL changes: every page keeps its address, so bookmarks and host links still work.
|
|
45
|
+
- One name, one meaning. The two "Approvals", "Documents", "Policies", "Benefits", "Tax" and "Loans" items are gone. HR's approval queues are tabs in Inbox (Leave requests, Comp-off, Attendance fixes, WFH, Home locations, Claims, Documents to verify, CA review), with their pending counts. Payroll and Payroll approval are one Pay › Payroll tab. "My work time", which had no menu link, is an Attendance tab.
|
|
46
|
+
- Phone: the bar holds the first four places; More lists the rest, 44 px tall with 8 px between rows (it was 2 px). The tab row scrolls sideways inside itself, never the page, and centres the current tab.
|
|
47
|
+
- Every tab is gated by the check its own page makes, so no menu link ends on "access denied". This fixes links the old menu showed to people who could not open them: Expenses, Benefits, Policies and Ask HR for someone without that permission (for example a CA), Organisation › Policies for leadership without `policy.manage`, and Roles for a superadmin without `role.manage`.
|
|
48
|
+
- `ApplicationHelper::NAV_ITEMS`, `ADMIN_NAV_ITEMS`, `LEADERSHIP_NAV_ITEMS` and `SUPERADMIN_NAV_ITEMS` are replaced by `SECTIONS`. The `admin/shared/_approvals_tabs` partial is removed; the layout's tab row replaces it.
|
|
49
|
+
|
|
50
|
+
## [0.37.3] - 2026-10-14
|
|
51
|
+
|
|
52
|
+
- Admin → Employees → a person now links to their Work time, Attendance and Documents next to Timeline and ID card. Each button shows only when the viewer can open that screen for that person.
|
|
53
|
+
|
|
54
|
+
## [0.37.2] - 2026-10-14
|
|
55
|
+
|
|
56
|
+
- Employee timeline no longer overlaps itself: the work-time day strip reused the `.hrl-timeline` class and squashed the timeline list to 2rem, so each month's events ran into the next month's heading. The day strip is now `.hrl-daystrip`.
|
|
57
|
+
|
|
10
58
|
## [0.37.1] - 2026-10-14
|
|
11
59
|
|
|
12
60
|
- A WFH request still waiting for HR now lets the person check in away from the office on that day (HR has 24 hours to decide). The day is flagged "WFH request pending HR approval: <reason>", and HR gets the flagged-punch alert with that reason. HR was already told when the request was raised. Rejected requests and requests for other dates are still refused.
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
|
|
13
13
|
centre(document.querySelector(".hrl-side"));
|
|
14
14
|
|
|
15
|
+
// The tab row scrolls sideways on a phone; bring the current tab into it.
|
|
16
|
+
var row = document.querySelector(".hrl-tabrow");
|
|
17
|
+
var tab = row && row.querySelector(".hrl-nav__link--active");
|
|
18
|
+
if (tab) row.scrollLeft += tab.getBoundingClientRect().left - row.getBoundingClientRect().left - (row.clientWidth - tab.offsetWidth) / 2;
|
|
19
|
+
|
|
15
20
|
// Chrome scrolls a focused field's caret into view, not the whole field, so a
|
|
16
21
|
// textarea near the bottom can sit half under the fixed tab bar. Nudge it out.
|
|
17
22
|
var tabbar = document.querySelector(".hrl-tabbar");
|
|
@@ -114,9 +114,18 @@ html:has(.hrl-body) { scroll-padding-top: 4rem; scroll-padding-bottom: calc(4.4r
|
|
|
114
114
|
.hrl-nav__link {
|
|
115
115
|
color: var(--hrl-muted); font-size: var(--hrl-t-small); padding: .5rem .6rem;
|
|
116
116
|
border-radius: var(--hrl-radius-sm); min-height: var(--hrl-target);
|
|
117
|
-
display: inline-flex; align-items: center;
|
|
117
|
+
display: inline-flex; align-items: center; gap: .6rem;
|
|
118
118
|
}
|
|
119
119
|
.hrl-nav__link--active { color: var(--hrl-accent); background: var(--hrl-info-bg); font-weight: 600; }
|
|
120
|
+
/* Place icons: stroke-only, decoration beside a label that always shows. */
|
|
121
|
+
.hrl-icon { width: 20px; height: 20px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
|
122
|
+
.hrl-nav__text { display: flex; flex-direction: column; min-width: 0; }
|
|
123
|
+
.hrl-nav__hint { font-size: var(--hrl-t-small); font-weight: 400; color: var(--hrl-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
124
|
+
.hrl-nav__link--active .hrl-nav__hint { color: inherit; }
|
|
125
|
+
/* Tab row: the current place's tabs on one line; it scrolls sideways, never the page. */
|
|
126
|
+
.hrl-tabrow { display: flex; gap: var(--hrl-gap-target); overflow-x: auto; margin: 0 0 var(--hrl-s-4); padding-bottom: var(--hrl-s-2); border-bottom: 1px solid var(--hrl-line); }
|
|
127
|
+
.hrl-tabrow .hrl-nav__link { flex: 0 0 auto; white-space: nowrap; }
|
|
128
|
+
.hrl-side__divider { height: 1px; background: var(--hrl-line); margin: var(--hrl-s-2) var(--hrl-s-1); }
|
|
120
129
|
|
|
121
130
|
/* Mobile app bar */
|
|
122
131
|
.hrl-appbar--mobile {
|
|
@@ -138,6 +147,12 @@ html:has(.hrl-body) { scroll-padding-top: 4rem; scroll-padding-bottom: calc(4.4r
|
|
|
138
147
|
padding: .25rem .25rem calc(.25rem + env(safe-area-inset-bottom));
|
|
139
148
|
}
|
|
140
149
|
.hrl-tabbar .hrl-nav__link { flex: 1 1 auto; min-width: var(--hrl-target); padding-inline: var(--hrl-s-1); justify-content: center; font-size: var(--hrl-t-micro); font-weight: 600; }
|
|
150
|
+
/* Bar items stack the icon over the label; the sheet's links stay in a row. */
|
|
151
|
+
.hrl-tabbar > .hrl-nav__link, .hrl-tabbar__more > summary { flex-direction: column; gap: 2px; padding-block: 2px; line-height: var(--hrl-lh-tight); }
|
|
152
|
+
.hrl-tabbar > .hrl-nav__link .hrl-icon, .hrl-tabbar__more > summary .hrl-icon { width: 22px; height: 22px; }
|
|
153
|
+
/* The Inbox count sits on the icon's corner so the bar keeps its height. */
|
|
154
|
+
.hrl-tabbar > .hrl-nav__link { position: relative; }
|
|
155
|
+
.hrl-tabbar > .hrl-nav__link .hrl-badge { position: absolute; top: 0; left: calc(50% + 2px); }
|
|
141
156
|
/* "More" sheet — everything five tabs cannot hold. Pure <details>: the
|
|
142
157
|
engine ships no Turbo or Stimulus and this must not need any. */
|
|
143
158
|
.hrl-tabbar__more { flex: 1 1 auto; display: flex; }
|
|
@@ -151,7 +166,7 @@ html:has(.hrl-body) { scroll-padding-top: 4rem; scroll-padding-bottom: calc(4.4r
|
|
|
151
166
|
position: fixed; left: 0; right: 0;
|
|
152
167
|
bottom: calc(3.3rem + env(safe-area-inset-bottom));
|
|
153
168
|
max-height: calc(100vh - 8rem - env(safe-area-inset-top) - env(safe-area-inset-bottom)); overflow-y: auto;
|
|
154
|
-
display: grid; grid-template-columns:
|
|
169
|
+
display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--hrl-gap-target);
|
|
155
170
|
background: var(--hrl-card); border-top: 1px solid var(--hrl-line);
|
|
156
171
|
border-radius: var(--hrl-radius-lg) var(--hrl-radius-lg) 0 0;
|
|
157
172
|
padding: .6rem .85rem;
|
|
@@ -161,15 +176,8 @@ html:has(.hrl-body) { scroll-padding-top: 4rem; scroll-padding-bottom: calc(4.4r
|
|
|
161
176
|
links and would otherwise apply here too. */
|
|
162
177
|
.hrl-sheet .hrl-nav__link {
|
|
163
178
|
display: flex; justify-content: flex-start;
|
|
164
|
-
min-height: var(--hrl-target); font-size: var(--hrl-t-body); padding: .5rem .6rem;
|
|
179
|
+
min-height: var(--hrl-target); min-width: 0; font-size: var(--hrl-t-body); padding: .5rem .6rem;
|
|
165
180
|
}
|
|
166
|
-
.hrl-sheet .hrl-side__group {
|
|
167
|
-
color: var(--hrl-muted); font-size: var(--hrl-t-micro); font-weight: 700;
|
|
168
|
-
text-transform: uppercase; letter-spacing: .06em;
|
|
169
|
-
margin: .7rem 0 .15rem; padding: 0 .6rem;
|
|
170
|
-
}
|
|
171
|
-
.hrl-sheet .hrl-side__group, .hrl-sheet .hrl-side__back { grid-column: 1 / -1; }
|
|
172
|
-
@media (min-width: 481px) { .hrl-sheet { grid-template-columns: 1fr; } }
|
|
173
181
|
/* Scrim over the page while the sheet is open; tapping it closes the sheet (nav.js). */
|
|
174
182
|
.hrl-tabbar__more[open]::before {
|
|
175
183
|
content: ""; position: fixed; inset: 0 0 calc(3.3rem + env(safe-area-inset-bottom));
|
|
@@ -191,7 +199,7 @@ html:has(.hrl-body) { scroll-padding-top: 4rem; scroll-padding-bottom: calc(4.4r
|
|
|
191
199
|
.hrl-side__brand { display: flex; align-items: center; min-height: var(--hrl-target); padding: .25rem .5rem 1rem; }
|
|
192
200
|
.hrl-side__logo { max-height: 40px; max-width: 100%; display: block; }
|
|
193
201
|
.hrl-side__brandname { font-family: var(--hrl-font-head); font-weight: 700; font-size: var(--hrl-t-lead); color: var(--hrl-ink); }
|
|
194
|
-
.hrl-side__nav { display: flex; flex-direction: column; gap:
|
|
202
|
+
.hrl-side__nav { display: flex; flex-direction: column; gap: var(--hrl-gap-target); }
|
|
195
203
|
.hrl-side__nav .hrl-nav__link { display: flex; padding: .45rem .6rem; }
|
|
196
204
|
.hrl-side__group {
|
|
197
205
|
margin: .9rem .5rem .25rem; font-size: var(--hrl-t-micro); font-weight: 700;
|
|
@@ -504,7 +512,6 @@ a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--
|
|
|
504
512
|
.hrl-right { margin-left: auto; }
|
|
505
513
|
.hrl-row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
|
|
506
514
|
.hrl-row--mb { margin-bottom: var(--hrl-s-3); }
|
|
507
|
-
.hrl-row--tabs > .hrl-btn { padding-inline: var(--hrl-s-2); } /* four approvals tabs fit one row beside the rail at 768px */
|
|
508
515
|
|
|
509
516
|
/* Long-form policy text. Constrained measure — a policy read across the
|
|
510
517
|
full width of a laptop is a policy nobody finishes. */
|
|
@@ -542,7 +549,7 @@ a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--
|
|
|
542
549
|
.hrl-idcard__photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
543
550
|
.hrl-photo-zoom { width: 100%; min-height: var(--hrl-target); accent-color: var(--hrl-accent); }
|
|
544
551
|
.hrl-idcard__who { margin-top: calc(124 * var(--u)); text-align: center; padding: 0 calc(24 * var(--u)); }
|
|
545
|
-
.hrl-idcard__name { font: 800 calc(25 * var(--u))/1.1 var(--hrl-card-display, var(--hrl-font)); letter-spacing: -.02em; text-wrap: balance; }
|
|
552
|
+
.hrl-idcard__name { font: 800 calc(25 * var(--u))/1.1 var(--hrl-card-display, var(--hrl-font)); letter-spacing: -.02em; text-wrap: balance; overflow-wrap: anywhere; }
|
|
546
553
|
.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; }
|
|
547
554
|
.hrl-idcard__green { color: var(--green); }
|
|
548
555
|
.hrl-idcard__blue { color: var(--blue); }
|
|
@@ -616,7 +623,7 @@ a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--
|
|
|
616
623
|
.hrl-chart-scroll { overflow-x: auto; }
|
|
617
624
|
.hrl-bars { width: 100%; min-width: 16rem; height: 10rem; }
|
|
618
625
|
.hrl-bars__label { font-size: 5px; fill: var(--hrl-muted); }
|
|
619
|
-
.hrl-
|
|
626
|
+
.hrl-daystrip { width: 100%; height: 2rem; background: var(--hrl-bg); border-radius: var(--hrl-radius-sm); }
|
|
620
627
|
.hrl-timeline__auto { stroke: var(--hrl-card); stroke-width: .3; stroke-dasharray: .6 .4; }
|
|
621
628
|
.hrl-timeline__axis { display: flex; justify-content: space-between; }
|
|
622
629
|
.hrl-timeline__list { margin: .5rem 0 0; padding-left: 1.2rem; display: grid; gap: .25rem; }
|
|
@@ -15,11 +15,7 @@ module HrLite
|
|
|
15
15
|
private
|
|
16
16
|
|
|
17
17
|
def require_operations_access!
|
|
18
|
-
|
|
19
|
-
hr_can?("attendance.manage", scope: :team) ||
|
|
20
|
-
hr_can?("leave.view", scope: :team)
|
|
21
|
-
|
|
22
|
-
hr_access_denied
|
|
18
|
+
hr_access_denied unless hr_operations?
|
|
23
19
|
end
|
|
24
20
|
end
|
|
25
21
|
end
|
|
@@ -10,7 +10,7 @@ module HrLite
|
|
|
10
10
|
before_action :hr_set_current_actor
|
|
11
11
|
around_action :hr_use_time_zone
|
|
12
12
|
|
|
13
|
-
helper_method :hr_current_user, :hr_admin?, :hr_leadership?, :hr_superadmin?, :hr_display_name,
|
|
13
|
+
helper_method :hr_current_user, :hr_admin?, :hr_leadership?, :hr_superadmin?, :hr_operations?, :hr_display_name,
|
|
14
14
|
:hr_can?, :hr_reaches?, :hr_access
|
|
15
15
|
|
|
16
16
|
private
|
|
@@ -72,6 +72,12 @@ module HrLite
|
|
|
72
72
|
HrLite.leadership?(hr_current_user)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
# Admin::BaseController's gate, and the menu's for its tabs: team scope is
|
|
76
|
+
# enough to reach the screen; each row is still checked by hr_require_reach!.
|
|
77
|
+
def hr_operations?
|
|
78
|
+
hr_can?("leave.approve", scope: :team) || hr_can?("attendance.manage", scope: :team) || hr_can?("leave.view", scope: :team)
|
|
79
|
+
end
|
|
80
|
+
|
|
75
81
|
def hr_display_name(user)
|
|
76
82
|
HrLite.display_name(user)
|
|
77
83
|
end
|
|
@@ -1,76 +1,103 @@
|
|
|
1
1
|
module HrLite
|
|
2
2
|
module ApplicationHelper
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
{ label: "
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{ label: "
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
3
|
+
# Who may see a tab. OPS is the operations pages' own check; the rest are the
|
|
4
|
+
# rules the old menu items had. Either way a tab never leads to a refusal.
|
|
5
|
+
MANAGE = -> { hr_admin? || hr_leadership? }
|
|
6
|
+
OPS = -> { hr_operations? }
|
|
7
|
+
MANAGE_OR_CLAIMS = -> { hr_admin? || hr_leadership? || hr_can?("expense.approve", scope: :team) }
|
|
8
|
+
LEAD = -> { hr_leadership? }
|
|
9
|
+
SUPER = -> { hr_superadmin? }
|
|
10
|
+
GEO = -> { HrLite.config.office_geofence }
|
|
11
|
+
|
|
12
|
+
# The menu: nine places, each with tabs. The rail and the phone bar list
|
|
13
|
+
# the places; the tab row lists the current place's tabs. A place shows
|
|
14
|
+
# when one of its tabs does, and opens on the first. One URL, one home.
|
|
15
|
+
SECTIONS = [
|
|
16
|
+
{ label: "Home", tabs: [
|
|
17
|
+
{ label: "Today", path: :root_path, match: [ "/" ] },
|
|
18
|
+
{ label: "Company overview", path: :admin_overview_path, match: [ "/admin/overview" ], if: OPS }
|
|
19
|
+
] },
|
|
20
|
+
{ label: "Attendance", tabs: [
|
|
21
|
+
{ label: "My attendance", path: :attendance_path, match: [ "/attendance", "/regularization_requests", "/wfh_requests", "/work_locations" ] },
|
|
22
|
+
{ label: "My work time", path: :work_time_path, match: [ "/work_time" ], if: GEO },
|
|
23
|
+
{ label: "Team", path: :admin_attendances_path, match: [ "/admin/attendances" ], if: OPS },
|
|
24
|
+
{ label: "Work modes", path: :admin_work_modes_path, match: [ "/admin/work_modes" ], if: -> { instance_exec(&GEO) && hr_operations? } },
|
|
25
|
+
{ label: "Team work time", path: :admin_work_time_path, match: [ "/admin/work_time" ],
|
|
26
|
+
if: -> { instance_exec(&GEO) && hr_can?("work_analytics.view", scope: :team) && hr_operations? } }
|
|
27
|
+
] },
|
|
28
|
+
{ label: "Leave", tabs: [
|
|
29
|
+
{ label: "My leave", path: :leave_requests_path, match: [ "/leave_requests", "/leave_balances", "/comp_off_requests" ] },
|
|
30
|
+
{ label: "Calendar", path: :calendar_path, match: [ "/calendar", "/holidays" ] },
|
|
31
|
+
{ label: "Team balances", path: :admin_leave_balances_path, match: [ "/admin/leave_balances" ], if: OPS }
|
|
32
|
+
] },
|
|
27
33
|
# Employee tier all the same — holding an approval IS the authorisation,
|
|
28
34
|
# so a manager and a stand-in covering for one reach the same screen.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
35
|
+
# `queue:` is what the tab's page lets this viewer decide, scoped as that
|
|
36
|
+
# page scopes it; the counts and the All list read it. All leads the place
|
|
37
|
+
# whenever any queue tab shows.
|
|
38
|
+
{ label: "Inbox", tabs: [
|
|
39
|
+
{ label: "All", lead: true, path: :approvals_path, match: [ "/approvals" ], if: -> { HrLite::ApprovalFlow.where(active: true).exists? } },
|
|
40
|
+
{ label: "Leave requests", path: :admin_leave_requests_path, match: [ "/admin/leave_requests" ], if: OPS,
|
|
41
|
+
queue: -> { hrl_queue(HrLite::LeaveRequest.where(status: "pending"), "leave.approve") } },
|
|
42
|
+
{ label: "Comp-off", path: :admin_comp_off_requests_path, match: [ "/admin/comp_off_requests" ], if: OPS,
|
|
43
|
+
queue: -> { hrl_queue(HrLite::CompOffRequest.pending, "leave.approve") } },
|
|
44
|
+
{ label: "Attendance fixes", path: :admin_regularization_requests_path, match: [ "/admin/regularization_requests" ], if: OPS,
|
|
45
|
+
queue: -> { hrl_queue(HrLite::RegularizationRequest.pending, "attendance.manage") } },
|
|
46
|
+
{ label: "WFH", path: :admin_wfh_requests_path, match: [ "/admin/wfh_requests" ], if: OPS,
|
|
47
|
+
queue: -> { hrl_queue(HrLite::WfhRequest.pending, "attendance.manage") } },
|
|
48
|
+
{ label: "Home locations", path: :admin_work_locations_path, match: [ "/admin/work_locations" ], if: OPS,
|
|
49
|
+
queue: -> { hrl_queue(HrLite::WorkLocation.pending, "attendance.manage") } },
|
|
50
|
+
{ label: "Claims", path: :admin_expenses_path, match: [ "/admin/expenses" ], if: -> { hr_can?("expense.approve", scope: :team) },
|
|
51
|
+
queue: -> { hrl_queue(HrLite::Expense.where(status: "submitted"), "expense.approve") } },
|
|
52
|
+
{ label: "Documents to verify", path: :admin_documents_path, match: [ "/admin/documents" ], if: LEAD,
|
|
53
|
+
queue: -> { hrl_queue(HrLite::Document.where(verification: "pending"), "document.view") } },
|
|
54
|
+
{ label: "CA review", path: :admin_ca_reviews_path, match: [ "/admin/ca_reviews" ],
|
|
55
|
+
if: -> { hr_can?("payroll.verify", scope: :all) || hr_can?("payroll.approve", scope: :all) } }
|
|
56
|
+
] },
|
|
57
|
+
{ label: "Pay", tabs: [
|
|
58
|
+
{ label: "Slips", path: :salary_slips_path, match: [ "/salary_slips" ] },
|
|
59
|
+
{ label: "Expenses", path: :expenses_path, match: [ "/expenses" ], if: -> { hr_can?("expense.claim") } },
|
|
60
|
+
{ label: "Tax", path: :tax_declaration_path, match: [ "/tax_declaration" ] },
|
|
61
|
+
{ label: "Loans", path: :loans_path, match: [ "/loans" ] },
|
|
62
|
+
{ label: "Benefits", path: :benefits_path, match: [ "/benefits" ], if: -> { hr_can?("benefit.view") } },
|
|
63
|
+
# HR approves payroll without running it; Super Admin runs it. One page.
|
|
64
|
+
{ label: "Payroll", path: :admin_payroll_runs_path, match: [ "/admin/payroll_runs", "/admin/salary_slips", "/admin/payroll_line_items" ],
|
|
65
|
+
if: -> { hr_superadmin? || (hr_can?("payroll.approve", scope: :all) && instance_exec(&MANAGE_OR_CLAIMS)) } },
|
|
66
|
+
{ label: "Tax checks", path: :admin_tax_declarations_path, match: [ "/admin/tax_declarations" ],
|
|
67
|
+
if: -> { hr_superadmin? || (hr_can?("tax.view", scope: :all) && instance_exec(&MANAGE_OR_CLAIMS)) } },
|
|
68
|
+
{ label: "Team loans", path: :admin_loans_path, match: [ "/admin/loans" ], if: SUPER },
|
|
69
|
+
{ label: "Team benefits", path: :admin_benefits_path, match: [ "/admin/benefits" ], if: MANAGE }
|
|
70
|
+
] },
|
|
71
|
+
{ label: "People", match: [ "/people" ], tabs: [
|
|
72
|
+
{ label: "My team", path: :team_path, match: [ "/team" ] },
|
|
73
|
+
{ label: "Org chart", path: :org_path, match: [ "/org" ] },
|
|
74
|
+
{ label: "Kudos", path: :kudos_path, match: [ "/kudos" ] },
|
|
75
|
+
{ label: "Employees", path: :admin_employees_path, match: [ "/admin/employees" ], if: LEAD },
|
|
76
|
+
{ label: "Joining & exits", path: :admin_checklists_path, match: [ "/admin/checklists" ], if: MANAGE },
|
|
77
|
+
{ label: "Awards", path: :admin_awards_path, match: [ "/admin/awards" ], if: LEAD },
|
|
78
|
+
{ label: "PIPs", path: :admin_performance_plans_path, match: [ "/admin/performance_plans" ], if: SUPER }
|
|
79
|
+
] },
|
|
80
|
+
{ label: "Me", tabs: [
|
|
81
|
+
{ label: "Profile", path: :employee_profile_path, match: [ "/profile", "/resignation" ] },
|
|
82
|
+
{ label: "Documents", path: :documents_path, match: [ "/documents" ] },
|
|
83
|
+
{ label: "ID card", path: :id_card_path, match: [ "/id_card" ] },
|
|
84
|
+
{ label: "Career", path: :career_path, match: [ "/career", "/appraisals" ] },
|
|
85
|
+
{ label: "Timeline", path: :timeline_path, match: [ "/timeline" ] }
|
|
86
|
+
] },
|
|
87
|
+
{ label: "Help", tabs: [
|
|
88
|
+
{ label: "Policies", path: :policies_path, match: [ "/policies" ], if: -> { hr_can?("policy.view", scope: :all) } },
|
|
89
|
+
{ label: "Ask HR", path: :hr_requests_path, match: [ "/hr_requests" ], if: -> { hr_can?("hr_request.raise") } },
|
|
90
|
+
{ label: "Help desk", path: :admin_hr_requests_path, match: [ "/admin/hr_requests" ], if: MANAGE },
|
|
91
|
+
{ label: "Manage policies", path: :admin_policies_path, match: [ "/admin/policies" ], if: -> { hr_can?("policy.manage", scope: :all) } }
|
|
92
|
+
] },
|
|
93
|
+
{ label: "Admin", divider: true, tabs: [
|
|
94
|
+
{ label: "Settings", path: :admin_leave_types_path, match: [ "/admin/leave_types", "/admin/office_locations", "/admin/holidays", "/admin/setting", "/admin/expense_categories" ], if: LEAD },
|
|
95
|
+
{ label: "Roles", path: :admin_roles_path, match: [ "/admin/roles" ], if: -> { hr_can?("role.manage", scope: :all) } },
|
|
96
|
+
{ label: "Statutory rates", path: :admin_statutory_rate_cards_path, match: [ "/admin/statutory_rate_cards" ], if: SUPER },
|
|
97
|
+
{ label: "Reports", path: :admin_reports_path, match: [ "/admin/reports" ], if: -> { hr_can?("expense.approve", scope: :all) || %w[leave.view attendance.view profile.view].any? { |key| hr_can?(key, scope: :team) } } },
|
|
98
|
+
{ label: "Assets", path: :admin_assets_path, match: [ "/admin/assets" ], if: MANAGE },
|
|
99
|
+
{ label: "Audit trail", path: :admin_audit_logs_path, match: [ "/admin/audit_logs" ], if: LEAD }
|
|
100
|
+
] }
|
|
74
101
|
].freeze
|
|
75
102
|
|
|
76
103
|
# DayStatus kind => [code, word, month-grid cell modifier]: the code keeps a state readable without its colour.
|
|
@@ -105,12 +132,65 @@ module HrLite
|
|
|
105
132
|
end
|
|
106
133
|
end
|
|
107
134
|
|
|
108
|
-
|
|
135
|
+
# A place carries its icon (tabs carry none); `hint` adds a second line under the label.
|
|
136
|
+
def hrl_nav_link(item, hint: nil)
|
|
109
137
|
active = hrl_nav_active?(item)
|
|
110
|
-
|
|
111
|
-
|
|
138
|
+
count = hrl_waiting(item)
|
|
139
|
+
text = hint ? tag.span(safe_join([ item[:label], tag.span(hint, class: "hrl-nav__hint") ]), class: "hrl-nav__text") : item[:label]
|
|
140
|
+
link_to hr_lite.public_send(item[:path]), class: "hrl-nav__link #{'hrl-nav__link--active' if active}", aria: { current: (item.fetch(:current, "page") if active) } do
|
|
141
|
+
safe_join([ (hrl_icon(item[:label]) if item[:tabs]), text, (tag.span(count, class: "hrl-badge hrl-badge--warn") if count&.positive?) ].compact, " ")
|
|
142
|
+
end
|
|
112
143
|
end
|
|
113
144
|
|
|
145
|
+
# How many wait in a tab's queue, or across a place's queue tabs; counted once per page.
|
|
146
|
+
def hrl_waiting(item)
|
|
147
|
+
queues = (item[:tabs] || [ item ]).select { |t| t[:queue] }
|
|
148
|
+
queues.sum { |t| (@hrl_waiting ||= {})[t[:label]] ||= instance_exec(&t[:queue]).count } if queues.any?
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# What a queue page lets this viewer decide: their reach, never their own requests.
|
|
152
|
+
def hrl_queue(relation, key) = hr_access.scope_relation(relation, key).where.not(user_id: hr_current_user.id)
|
|
153
|
+
|
|
154
|
+
# Everything waiting in this viewer's queues, oldest first, as [tab, record].
|
|
155
|
+
def hrl_inbox_rows(limit = 50)
|
|
156
|
+
inbox = hrl_sections.find { |s| s[:label] == "Inbox" } or return []
|
|
157
|
+
inbox[:tabs].select { |t| t[:queue] }
|
|
158
|
+
.flat_map { |t| instance_exec(&t[:queue]).includes(:user).order(:created_at).limit(limit).map { |r| [ t, r ] } }
|
|
159
|
+
.sort_by { |_, r| r.created_at }.first(limit)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# One stroke icon per place. Decoration only: the label always shows beside it.
|
|
163
|
+
ICONS = {
|
|
164
|
+
"Home" => '<path d="M3 10.5 12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/>',
|
|
165
|
+
"Attendance" => '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
|
|
166
|
+
"Leave" => '<rect x="3" y="4" width="18" height="17" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/>',
|
|
167
|
+
"Inbox" => '<path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.5 5.1 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.5-6.9A2 2 0 0 0 16.8 4H7.2a2 2 0 0 0-1.7 1.1z"/>',
|
|
168
|
+
"Pay" => '<rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20M6 15h4"/>',
|
|
169
|
+
"People" => '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8"/>',
|
|
170
|
+
"Me" => '<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>',
|
|
171
|
+
"Help" => '<circle cx="12" cy="12" r="9"/><path d="M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01"/>',
|
|
172
|
+
"Admin" => '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
|
|
173
|
+
"More" => '<circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/>'
|
|
174
|
+
}.freeze
|
|
175
|
+
|
|
176
|
+
def hrl_icon(name)
|
|
177
|
+
paths = ICONS[name] or return
|
|
178
|
+
tag.svg(paths.html_safe, class: "hrl-icon", viewBox: "0 0 24 24", aria: { hidden: true }, focusable: "false") # rubocop:disable Rails/OutputSafety -- constant markup above
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Visible places, each with its visible tabs; a place links to its first
|
|
182
|
+
# tab and lights for any tab's paths (plus its own, e.g. a colleague's timeline).
|
|
183
|
+
def hrl_sections
|
|
184
|
+
@hrl_sections ||= SECTIONS.filter_map do |section|
|
|
185
|
+
rest = hrl_nav_items(section[:tabs].reject { |t| t[:lead] })
|
|
186
|
+
# A lead tab also shows whenever any other tab in its place does.
|
|
187
|
+
tabs = section[:tabs].select { |t| t[:lead] && hr_lite_route?(t[:path]) && (rest.any? || instance_exec(&t[:if])) } + rest
|
|
188
|
+
section.merge(tabs: tabs, path: tabs.first[:path], current: "true", match: tabs.flat_map { |t| t[:match] } + section.fetch(:match, [])) if tabs.any?
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def hrl_current_section = hrl_sections.find { |section| hrl_nav_active?(section) }
|
|
193
|
+
|
|
114
194
|
# Mention-marker-aware, XSS-safe kudos message rendering. Every literal
|
|
115
195
|
# chunk is escaped; only markers whose id has a mention row become
|
|
116
196
|
# highlighted chips (with the CURRENT display name — stale-name safe).
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<% content_for(:page_title) { "Attendance — #{hr_display_name(@employee)}" } %>
|
|
2
|
+
<% content_for(:tabrow) { render "hr_lite/shared/person_tabs", user: @employee } %>
|
|
2
3
|
<div class="hrl-page__head">
|
|
3
4
|
<h1 class="hrl-page__title"><%= hr_display_name(@employee) %></h1>
|
|
4
5
|
<div class="hrl-page__actions">
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
<h1 class="hrl-page__title">Comp-off approvals</h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
|
-
<%= render "hr_lite/admin/shared/approvals_tabs", active: :comp_off %>
|
|
7
|
-
|
|
8
6
|
<div class="hrl-row hrl-row--mb">
|
|
9
7
|
<span class="hrl-small hrl-muted">Status</span>
|
|
10
8
|
<% HrLite::CompOffRequest::STATUSES.each do |status| %>
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
<% content_for(:page_title) { @profile.employee_code } %>
|
|
2
|
+
<% content_for(:tabrow) { render "hr_lite/shared/person_tabs", user: @profile.user } %>
|
|
2
3
|
<div class="hrl-page__head">
|
|
3
4
|
<h1 class="hrl-page__title"><%= hr_display_name(@profile.user) %> <span class="hrl-muted">(<%= @profile.employee_code %>)</span></h1>
|
|
4
5
|
<div class="hrl-page__actions">
|
|
5
6
|
<a class="hrl-btn" href="<%= hr_lite.edit_admin_employee_path(@profile) %>">Edit</a>
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
<%# Their records are the tabs above; the HR actions fold into one Manage button. %>
|
|
8
|
+
<% if hr_superadmin? || hr_can?("salary.manage", scope: :all) %>
|
|
9
|
+
<details>
|
|
10
|
+
<summary class="hrl-btn">Manage</summary>
|
|
11
|
+
<div class="hrl-row">
|
|
12
|
+
<% if hr_superadmin? %>
|
|
13
|
+
<a class="hrl-btn" href="<%= hr_lite.new_admin_employee_appraisal_path(@profile) %>">New appraisal</a>
|
|
14
|
+
<a class="hrl-btn" href="<%= hr_lite.new_admin_employee_designation_change_path(@profile) %>">Role change</a>
|
|
15
|
+
<a class="hrl-btn" href="<%= hr_lite.new_admin_performance_plan_path(user_id: @profile.user_id) %>">Start PIP</a>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% if hr_can?("salary.manage", scope: :all) %>
|
|
18
|
+
<a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_admin_employee_salary_structure_path(@profile) %>">New structure</a>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
</details>
|
|
15
22
|
<% end %>
|
|
16
23
|
</div>
|
|
17
24
|
</div>
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
<h1 class="hrl-page__title">Team leave balances <%= HrLite::LeaveYear.label(@year) %></h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
|
-
<%= render "hr_lite/admin/shared/approvals_tabs", active: :balances %>
|
|
7
|
-
|
|
8
6
|
<section class="hrl-card">
|
|
9
7
|
<div class="hrl-table-wrap">
|
|
10
8
|
<table class="hrl-table hrl-table--stack">
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
<h1 class="hrl-page__title">Regularization tickets</h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
|
-
<%= render "hr_lite/admin/shared/approvals_tabs", active: :regularizations %>
|
|
7
|
-
|
|
8
6
|
<div class="hrl-row hrl-row--mb">
|
|
9
7
|
<span class="hrl-small hrl-muted">Status</span>
|
|
10
8
|
<% HrLite::RegularizationRequest::STATUSES.each do |status| %>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<% content_for(:page_title) { "Tax — #{hr_display_name(@person)}" } %>
|
|
2
|
+
<% content_for(:tabrow) { render "hr_lite/shared/person_tabs", user: @person } %>
|
|
2
3
|
<div class="hrl-page__head">
|
|
3
4
|
<h1 class="hrl-page__title">Tax — <%= hr_display_name(@person) %></h1>
|
|
4
5
|
<div class="hrl-page__actions">
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
<h1 class="hrl-page__title">Work from home</h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
|
-
<%= render "hr_lite/admin/shared/approvals_tabs", active: :wfh %>
|
|
7
|
-
|
|
8
6
|
<section class="hrl-card">
|
|
9
7
|
<%= form_with url: hr_lite.admin_wfh_requests_path, method: :get, local: true do %>
|
|
10
8
|
<div class="hrl-field"><label for="wfh-from">From</label><%= date_field_tag :from, @from, id: "wfh-from" %></div>
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
<h1 class="hrl-page__title">WFH locations</h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
|
-
<%= render "hr_lite/admin/shared/approvals_tabs", active: :locations %>
|
|
7
|
-
|
|
8
6
|
<div class="hrl-row hrl-row--mb">
|
|
9
7
|
<span class="hrl-small hrl-muted">Status</span>
|
|
10
8
|
<% HrLite::WorkLocation::STATUSES.each do |status| %>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<% content_for(:page_title) { "Work time" } %>
|
|
2
|
+
<% content_for(:tabrow) { render "hr_lite/shared/person_tabs", user: @person } if @person %>
|
|
2
3
|
<div class="hrl-page__head">
|
|
3
4
|
<h1 class="hrl-page__title">Work time<%= " — #{@names[@person.id]}" if @person %></h1>
|
|
4
5
|
<% if @person %>
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
</section>
|
|
14
14
|
<% end %>
|
|
15
15
|
|
|
16
|
+
<% rows = hrl_inbox_rows %>
|
|
16
17
|
<section class="hrl-card">
|
|
17
|
-
<% if @approvals.empty? %>
|
|
18
|
+
<% if @approvals.empty? && rows.empty? %>
|
|
18
19
|
<p class="hrl-muted">Nothing is waiting on you.</p>
|
|
19
20
|
<% else %>
|
|
20
21
|
<div class="hrl-table-wrap">
|
|
@@ -44,6 +45,15 @@
|
|
|
44
45
|
</td>
|
|
45
46
|
</tr>
|
|
46
47
|
<% end %>
|
|
48
|
+
<%# The HR queues this viewer decides; Open lands on that queue's own page. %>
|
|
49
|
+
<% rows.each do |tab, record| %>
|
|
50
|
+
<tr>
|
|
51
|
+
<td data-label="Request"><strong><%= tab[:label] %></strong></td>
|
|
52
|
+
<td data-label="From"><%= hr_display_name(record.user) %></td>
|
|
53
|
+
<td data-label="Waiting"><%= time_ago_in_words(record.created_at) %></td>
|
|
54
|
+
<td data-label=""><a class="hrl-btn" href="<%= hr_lite.public_send(tab[:path]) %>">Open</a></td>
|
|
55
|
+
</tr>
|
|
56
|
+
<% end %>
|
|
47
57
|
</tbody>
|
|
48
58
|
</table>
|
|
49
59
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<% content_for(:page_title) { "ID card" } %>
|
|
2
|
+
<% content_for(:tabrow) { render "hr_lite/shared/person_tabs", user: @user } if params[:user_id] %>
|
|
2
3
|
<% company = HrLite.config.company.call %>
|
|
3
4
|
<% brand = company[:brand].presence || company[:name].presence || "HR" %>
|
|
4
5
|
<% own = @user == hr_current_user %>
|
|
@@ -29,7 +30,8 @@
|
|
|
29
30
|
<% end %>
|
|
30
31
|
</div>
|
|
31
32
|
<div class="hrl-idcard__who">
|
|
32
|
-
|
|
33
|
+
<%# An email standing in for the name breaks before its "@", else anywhere (CSS). %>
|
|
34
|
+
<div class="hrl-idcard__name"><span class="hrl-idcard__green"><%= safe_join(first.to_s.split("@", 2), tag.wbr + "@") %></span> <span class="hrl-idcard__blue"><%= rest %></span></div>
|
|
33
35
|
<div class="hrl-idcard__role"><%= @profile.designation %></div>
|
|
34
36
|
</div>
|
|
35
37
|
<dl class="hrl-idcard__facts">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<% content_for(:page_title) { "Leave balances" } %>
|
|
2
|
-
<% content_for(:back, "
|
|
2
|
+
<% content_for(:back, "Leave") %>
|
|
3
3
|
<div class="hrl-page__head">
|
|
4
4
|
<h1 class="hrl-page__title">Leave balances <%= HrLite::LeaveYear.label(@year) %></h1>
|
|
5
5
|
<div class="hrl-page__actions">
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<%# locals: user — one person's records as tabs. Each shows only when its page would open for this viewer. %>
|
|
2
|
+
<% ops = hr_operations?
|
|
3
|
+
profile = HrLite::EmployeeProfile.find_by(user_id: user.id)
|
|
4
|
+
tabs = [
|
|
5
|
+
[ "Overview", (hr_lite.admin_employee_path(profile) if profile && hr_leadership?) ],
|
|
6
|
+
[ "Attendance", (hr_lite.admin_attendance_path(user.id) if ops && hr_reaches?("attendance.view", user)) ],
|
|
7
|
+
[ "Work time", (hr_lite.admin_work_time_path(user_id: user.id) if HrLite.config.office_geofence && ops && hr_can?("work_analytics.view", scope: :team) && hr_reaches?("work_analytics.view", user)) ],
|
|
8
|
+
[ "Documents", (hr_lite.admin_employee_documents_path(user.id) if hr_can?("document.view", scope: :team) && hr_reaches?("document.view", user)) ],
|
|
9
|
+
[ "Tax", (hr_lite.person_admin_tax_declarations_path(user.id) if hr_can?("tax.view", scope: :all) && hr_reaches?("tax.view", user)) ],
|
|
10
|
+
[ "Timeline", hr_lite.person_timeline_path(user.id) ],
|
|
11
|
+
[ "ID card", (hr_lite.id_card_path(user_id: user.id) if user == hr_current_user || hr_reaches?("profile.view", user)) ]
|
|
12
|
+
].select(&:last) %>
|
|
13
|
+
<%# One tab is no choice: render nothing and the layout keeps the place's own tab row. %>
|
|
14
|
+
<% if tabs.many? %>
|
|
15
|
+
<nav class="hrl-tabrow" aria-label="<%= hr_display_name(user) %>'s records">
|
|
16
|
+
<% tabs.each do |label, url| %>
|
|
17
|
+
<% on = request.path == url.split("?").first %>
|
|
18
|
+
<%= link_to label, url, class: [ "hrl-nav__link", ("hrl-nav__link--active" if on) ], aria: { current: ("page" if on) } %>
|
|
19
|
+
<% end %>
|
|
20
|
+
</nav>
|
|
21
|
+
<% end %>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<% content_for(:page_title) { "Timeline — #{hr_display_name(@person)}" } %>
|
|
2
|
+
<% content_for(:tabrow) { render "hr_lite/shared/person_tabs", user: @person } if params[:user_id] %>
|
|
2
3
|
<% 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
4
|
<% name = hr_display_name(@person) %>
|
|
4
5
|
<div class="hrl-page__head">
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<h2 class="hrl-card__title">The day, hour by hour</h2>
|
|
7
7
|
<% rows.each do |user_id, segs| %>
|
|
8
8
|
<% if names %><p class="hrl-small"><strong><%= names[user_id] %></strong></p><% end %>
|
|
9
|
-
<svg class="hrl-
|
|
9
|
+
<svg class="hrl-daystrip" viewBox="0 0 100 10" preserveAspectRatio="none" role="img" aria-label="Work modes through the day; details in the list below">
|
|
10
10
|
<% segs.each do |seg| %>
|
|
11
11
|
<rect x="<%= pct.(seg.from).round(3) %>" y="1" width="<%= [ pct.(seg.to) - pct.(seg.from), 0.15 ].max.round(3) %>" height="8"
|
|
12
12
|
class="<%= class_names("hrl-fill--#{seg.mode}", "hrl-timeline__auto": seg.source == "system") %>" data-hrl-tip tabindex="0">
|
|
@@ -26,34 +26,9 @@
|
|
|
26
26
|
</a>
|
|
27
27
|
|
|
28
28
|
<nav class="hrl-side__nav">
|
|
29
|
-
<%
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<%= hrl_nav_link(item) %>
|
|
33
|
-
<% end %>
|
|
34
|
-
<% end %>
|
|
35
|
-
|
|
36
|
-
<% if hr_admin? || hr_leadership? || hr_can?("expense.approve", scope: :team) %>
|
|
37
|
-
<span class="hrl-side__group">Manage</span>
|
|
38
|
-
<% hrl_nav_items(HrLite::ApplicationHelper::ADMIN_NAV_ITEMS).each do |item| %>
|
|
39
|
-
<%= hrl_nav_link(item) if item[:if] || hr_admin? || hr_leadership? %>
|
|
40
|
-
<% end %>
|
|
41
|
-
<% end %>
|
|
42
|
-
|
|
43
|
-
<% if hr_leadership? || hr_superadmin? %>
|
|
44
|
-
<span class="hrl-side__group">Organisation</span>
|
|
45
|
-
<% if hr_leadership? %>
|
|
46
|
-
<% hrl_nav_items(HrLite::ApplicationHelper::LEADERSHIP_NAV_ITEMS).each do |item| %>
|
|
47
|
-
<%= hrl_nav_link(item) %>
|
|
48
|
-
<% end %>
|
|
49
|
-
<% end %>
|
|
50
|
-
<%# Payroll hangs off the money tier alone — a superadmin who is not
|
|
51
|
-
in leadership_emails still needs to reach it. %>
|
|
52
|
-
<% if hr_superadmin? %>
|
|
53
|
-
<% hrl_nav_items(HrLite::ApplicationHelper::SUPERADMIN_NAV_ITEMS).each do |item| %>
|
|
54
|
-
<%= hrl_nav_link(item) %>
|
|
55
|
-
<% end %>
|
|
56
|
-
<% end %>
|
|
29
|
+
<% hrl_sections.each do |section| %>
|
|
30
|
+
<% if section[:divider] %><span class="hrl-side__divider" aria-hidden="true"></span><% end %>
|
|
31
|
+
<%= hrl_nav_link(section) %>
|
|
57
32
|
<% end %>
|
|
58
33
|
</nav>
|
|
59
34
|
|
|
@@ -81,8 +56,16 @@
|
|
|
81
56
|
</header>
|
|
82
57
|
|
|
83
58
|
<main class="hrl-main" id="hrl-main" tabindex="-1">
|
|
84
|
-
<%# A
|
|
85
|
-
<% if
|
|
59
|
+
<%# A page about one person puts that person's tabs here instead (shared/_person_tabs). %>
|
|
60
|
+
<% if content_for?(:tabrow) %>
|
|
61
|
+
<%= yield :tabrow %>
|
|
62
|
+
<% elsif (section = hrl_current_section) && section[:tabs].many? %>
|
|
63
|
+
<nav class="hrl-tabrow" aria-label="<%= section[:label] %> sections">
|
|
64
|
+
<% section[:tabs].each do |tab| %><%= hrl_nav_link(tab) %><% end %>
|
|
65
|
+
</nav>
|
|
66
|
+
<% end %>
|
|
67
|
+
<%# A sub-screen names its parent place or tab: content_for(:back, "Leave"). %>
|
|
68
|
+
<% if (tab = (hrl_sections + hrl_sections.flat_map { |s| s[:tabs] }).find { |i| i[:label] == content_for(:back) }) %>
|
|
86
69
|
<a class="hrl-backlink" href="<%= hr_lite.public_send(tab[:path]) %>"><span aria-hidden="true">‹</span> <%= tab[:label] %></a>
|
|
87
70
|
<% end %>
|
|
88
71
|
<% if flash[:notice].present? %><div class="hrl-flash hrl-flash--ok" role="status"><%= flash[:notice] %></div><% end %>
|
|
@@ -100,41 +83,20 @@
|
|
|
100
83
|
leadership and payroll screen were unreachable. <details> keeps it
|
|
101
84
|
dependency-free; the engine ships no Turbo or Stimulus. %>
|
|
102
85
|
<nav class="hrl-tabbar" aria-label="HR sections">
|
|
103
|
-
<% primary, rest =
|
|
104
|
-
<% primary.each do |
|
|
105
|
-
<%= hrl_nav_link(
|
|
86
|
+
<% primary, rest = hrl_sections.partition.with_index { |_, i| i < 4 } %>
|
|
87
|
+
<% primary.each do |section| %>
|
|
88
|
+
<%= hrl_nav_link(section) %>
|
|
106
89
|
<% end %>
|
|
107
90
|
<details class="hrl-tabbar__more">
|
|
108
|
-
<% lit = rest.any? { |
|
|
109
|
-
<%= tag.summary "More", class: [ "hrl-nav__link", ("hrl-nav__link--active" if lit) ], aria: { current: ("true" if lit) } %>
|
|
91
|
+
<% lit = rest.any? { |s| hrl_nav_active?(s) } %>
|
|
92
|
+
<%= tag.summary safe_join([ hrl_icon("More"), "More" ]), class: [ "hrl-nav__link", ("hrl-nav__link--active" if lit) ], aria: { current: ("true" if lit) } %>
|
|
110
93
|
<div class="hrl-sheet">
|
|
111
94
|
<% if HrLite.config.back_link %>
|
|
112
95
|
<a class="hrl-nav__link hrl-side__back" href="<%= HrLite.config.back_link[:url] %>"><%= HrLite.config.back_link[:label] %></a>
|
|
113
96
|
<% end %>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
<%= hrl_nav_link(item) %>
|
|
118
|
-
<% end %>
|
|
119
|
-
<% end %>
|
|
120
|
-
<% if hr_admin? || hr_leadership? || hr_can?("expense.approve", scope: :team) %>
|
|
121
|
-
<span class="hrl-side__group">Manage</span>
|
|
122
|
-
<% hrl_nav_items(HrLite::ApplicationHelper::ADMIN_NAV_ITEMS).each do |item| %>
|
|
123
|
-
<%= hrl_nav_link(item) if item[:if] || hr_admin? || hr_leadership? %>
|
|
124
|
-
<% end %>
|
|
125
|
-
<% end %>
|
|
126
|
-
<% if hr_leadership? || hr_superadmin? %>
|
|
127
|
-
<span class="hrl-side__group">Organisation</span>
|
|
128
|
-
<% if hr_leadership? %>
|
|
129
|
-
<% hrl_nav_items(HrLite::ApplicationHelper::LEADERSHIP_NAV_ITEMS).each do |item| %>
|
|
130
|
-
<%= hrl_nav_link(item) %>
|
|
131
|
-
<% end %>
|
|
132
|
-
<% end %>
|
|
133
|
-
<% if hr_superadmin? %>
|
|
134
|
-
<% hrl_nav_items(HrLite::ApplicationHelper::SUPERADMIN_NAV_ITEMS).each do |item| %>
|
|
135
|
-
<%= hrl_nav_link(item) %>
|
|
136
|
-
<% end %>
|
|
137
|
-
<% end %>
|
|
97
|
+
<%# The second line names the place's tabs, so a phone user knows what is inside before opening it. %>
|
|
98
|
+
<% rest.each do |section| %>
|
|
99
|
+
<%= hrl_nav_link(section, hint: section[:tabs].map { |t| t[:label] }.join(", ")) %>
|
|
138
100
|
<% end %>
|
|
139
101
|
</div>
|
|
140
102
|
</details>
|
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.40.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kshitiz sinha
|
|
@@ -330,7 +330,6 @@ files:
|
|
|
330
330
|
- app/views/hr_lite/admin/salary_structures/edit.html.erb
|
|
331
331
|
- app/views/hr_lite/admin/salary_structures/new.html.erb
|
|
332
332
|
- app/views/hr_lite/admin/settings/edit.html.erb
|
|
333
|
-
- app/views/hr_lite/admin/shared/_approvals_tabs.html.erb
|
|
334
333
|
- app/views/hr_lite/admin/statutory_rate_cards/_form.html.erb
|
|
335
334
|
- app/views/hr_lite/admin/statutory_rate_cards/edit.html.erb
|
|
336
335
|
- app/views/hr_lite/admin/statutory_rate_cards/index.html.erb
|
|
@@ -402,6 +401,7 @@ files:
|
|
|
402
401
|
- app/views/hr_lite/shared/_bell.html.erb
|
|
403
402
|
- app/views/hr_lite/shared/_form_errors.html.erb
|
|
404
403
|
- app/views/hr_lite/shared/_pagination.html.erb
|
|
404
|
+
- app/views/hr_lite/shared/_person_tabs.html.erb
|
|
405
405
|
- app/views/hr_lite/tax/_computation.html.erb
|
|
406
406
|
- app/views/hr_lite/tax_declarations/show.html.erb
|
|
407
407
|
- app/views/hr_lite/team/show.html.erb
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
<%# locals: active (:leaves | :comp_off | :regularizations | :wfh | :locations | :balances) %>
|
|
2
|
-
<% tab = ->(key) { tag.attributes(class: [ "hrl-btn", ("hrl-btn--primary" if active == key) ], aria: { current: ("page" if active == key) }) } %>
|
|
3
|
-
<div class="hrl-row hrl-row--mb hrl-row--tabs">
|
|
4
|
-
<a <%= tab.(:leaves) %>
|
|
5
|
-
href="<%= hr_lite.admin_leave_requests_path %>">Leaves</a>
|
|
6
|
-
<a <%= tab.(:comp_off) %>
|
|
7
|
-
href="<%= hr_lite.admin_comp_off_requests_path %>">Comp-off
|
|
8
|
-
<% if (n = HrLite::CompOffRequest.pending.count).positive? %><span class="hrl-badge hrl-badge--warn"><%= n %></span><% end %></a>
|
|
9
|
-
<a <%= tab.(:regularizations) %>
|
|
10
|
-
href="<%= hr_lite.admin_regularization_requests_path %>">Regularization
|
|
11
|
-
<% if (n = HrLite::RegularizationRequest.pending.count).positive? %><span class="hrl-badge hrl-badge--warn"><%= n %></span><% end %></a>
|
|
12
|
-
<a <%= tab.(:wfh) %>
|
|
13
|
-
href="<%= hr_lite.admin_wfh_requests_path %>">WFH
|
|
14
|
-
<% if (n = HrLite::WfhRequest.pending.count).positive? %><span class="hrl-badge hrl-badge--warn"><%= n %></span><% end %></a>
|
|
15
|
-
<a <%= tab.(:locations) %>
|
|
16
|
-
href="<%= hr_lite.admin_work_locations_path %>">Locations
|
|
17
|
-
<% if (n = HrLite::WorkLocation.pending.count).positive? %><span class="hrl-badge hrl-badge--warn"><%= n %></span><% end %></a>
|
|
18
|
-
<a <%= tab.(:balances) %>
|
|
19
|
-
href="<%= hr_lite.admin_leave_balances_path %>">Balances</a>
|
|
20
|
-
</div>
|