hr_lite 0.48.2 → 0.51.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 +24 -0
- data/app/assets/javascripts/hr_lite/search.js +48 -0
- data/app/assets/stylesheets/hr_lite/hr_lite.css +5 -0
- data/app/controllers/hr_lite/admin/attendances_controller.rb +54 -0
- data/app/controllers/hr_lite/admin/employees_controller.rb +4 -1
- data/app/controllers/hr_lite/search_controller.rb +42 -0
- data/app/helpers/hr_lite/application_helper.rb +2 -1
- data/app/models/hr_lite/attendance_record.rb +2 -2
- data/app/models/hr_lite/employee_profile.rb +7 -2
- data/app/models/hr_lite/repeat_rule.rb +74 -0
- data/app/views/hr_lite/admin/attendances/summary.html.erb +93 -0
- data/app/views/hr_lite/admin/employees/_form.html.erb +1 -5
- data/app/views/hr_lite/admin/employees/_shorter_day_fields.html.erb +30 -0
- data/app/views/hr_lite/attendance/_punch_card.html.erb +2 -2
- data/app/views/hr_lite/search/_results.html.erb +14 -0
- data/app/views/hr_lite/search/show.html.erb +9 -0
- data/app/views/hr_lite/shared/_person_tabs.html.erb +1 -0
- data/app/views/layouts/hr_lite/application.html.erb +11 -1
- data/config/routes.rb +2 -1
- data/db/migrate/20261016100000_add_hours_allowance_repeat_to_hr_lite_employee_profiles.rb +5 -0
- data/lib/hr_lite/version.rb +1 -1
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 29f3f6694be039bef5c21d9548eaf1aaf8ff7c60191b0e92e2903c9fac7769fe
|
|
4
|
+
data.tar.gz: 361260627747e8ca8280401e985ef777cc8b267864fc6ccc0229ddf84670e939
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fbe31ca225879b95e882ee76b5cca183ef535c124c75616ebde7cd350bc03bff252febf09d6e7375f0dcf395ab19593548140667e6aa358b707be8f405c0f6f3
|
|
7
|
+
data.tar.gz: 39472b4fffa414eaafcbb2a7fb366354608d79f91f1cabe3e41d1e5a32380a2f7b42460fe3539e726850e163cea5680b5531aa4edf5ab7e79449d16e425e4091
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.51.0] - 2026-10-16
|
|
11
|
+
|
|
12
|
+
- Shorter day on a repeat: HR or admin give a person 30, 60 or 90 minutes off the working day (so 7h at the least) on a pattern set like a calendar's custom repeat: every N days, every N weeks on chosen weekdays, or every N months on the start's date, from a start date, ending never, on a date, or after N times. No repeat means every day, as before.
|
|
13
|
+
- Set it on the person's This week screen (Shorter day card) or on their employee profile; both edit the same fields. The card shows the pattern to anyone who can open the screen. The form and the reason show only to those who manage profiles company-wide, and never on your own record.
|
|
14
|
+
- The day's required hours, the short-day check and the "Your day" badge follow the pattern, and This week marks each day it covers with "Day 7h 00m".
|
|
15
|
+
- Migration: `hr_lite_employee_profiles.hours_allowance_repeat` (string). Hosts rebuild their schema on the bump.
|
|
16
|
+
|
|
17
|
+
## [0.50.0] - 2026-10-16
|
|
18
|
+
|
|
19
|
+
- Global search: one box for screens and people. It opens with ⌘K or Ctrl+K anywhere, from **Search** at the top of the sidebar, or from **Search** in the phone's More sheet. Typing (2 letters or more) lists matching screens and people; arrows move, Enter opens, and Escape closes. With JavaScript off, Search opens the `/search` page.
|
|
20
|
+
- Screens come from the menu's own gates, so a person only finds what they can already open. A screen matches on its place, its tab and its paths ("wfh" finds My attendance through `/wfh_requests`), plus a few everyday words such as "payslip" or "penalty".
|
|
21
|
+
- People are listed only for those with a team screen, and only people whose attendance they may open: a manager finds their team, HR finds everyone, and an employee finds no one. Search covers name, email and employee code. A person opens on their This week screen. A page outside someone's reach still answers "not allowed", exactly as before.
|
|
22
|
+
- The endpoint has a named rate limit (60 a minute per person).
|
|
23
|
+
|
|
24
|
+
## [0.49.0] - 2026-10-16
|
|
25
|
+
|
|
26
|
+
- "This week": one screen per employee for HR, admin and managers. It's the first tab in the person's tabs, at `/admin/attendances/:user_id/summary`. Day, Week (the default) or Month, with ‹ › and a "Go to date" calendar. It shows:
|
|
27
|
+
- every day's status, check-in and check-out (all punches when there was more than one pair), hours worked, penalty minutes and flags
|
|
28
|
+
- every request in the period: leave, comp-off, WFH, attendance fixes, expense claims and uploaded documents, each with its status and an "Open" link to where it's decided
|
|
29
|
+
- leave balances, with any days held for penalties
|
|
30
|
+
- the penalty-hours section, with Remove hours for those who may manage the person
|
|
31
|
+
- Each past or current day has a "Fix" button that goes straight to that day's Fix card, including reset and penalties.
|
|
32
|
+
- Access is unchanged. The screen needs attendance.view for the person; a manager sees only their team. Each request type shows only if the viewer may open that type's own page for this person: leave and comp-off need leave.view, WFH and fixes need attendance.view, claims need expense.approve, documents need document.view. So HR, which can't approve claims, sees no claims here. Fix and Remove hours need attendance.manage, and never for your own record.
|
|
33
|
+
|
|
10
34
|
## [0.48.2] - 2026-10-16
|
|
11
35
|
|
|
12
36
|
- Breaks count as working time. The standard day is 8h 30m including its break, but My work time (and HR's work time report) showed "Working time 8h 00m" for a full 8h 30m shift, because lunch and short breaks were left out. The KPI now reads "Working time (breaks included)" (8h 30m for that shift), and breaks show as "Of which breaks". The daily table's columns are "Working (breaks incl.)" and "Of which breaks". Only the display changed: attendance, penalties, half day and pay already used check-in to check-out.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// The search palette: ⌘K / Ctrl+K or any [data-hrl-search-open] link opens it.
|
|
2
|
+
// Typing loads the short results from /search?frame=1 after a pause; Enter
|
|
3
|
+
// opens the full page; arrows move through results; Escape closes (native).
|
|
4
|
+
// Without JS (or <dialog>) the links simply open the search page.
|
|
5
|
+
(function () {
|
|
6
|
+
var dialog = document.querySelector("[data-hrl-search]");
|
|
7
|
+
if (!dialog || !dialog.showModal) return;
|
|
8
|
+
var form = dialog.querySelector("form");
|
|
9
|
+
var input = form.querySelector("input");
|
|
10
|
+
var out = dialog.querySelector("[data-hrl-search-results]");
|
|
11
|
+
var timer;
|
|
12
|
+
|
|
13
|
+
function open(event) {
|
|
14
|
+
if (event) event.preventDefault();
|
|
15
|
+
var more = document.querySelector(".hrl-tabbar__more[open]");
|
|
16
|
+
if (more) more.open = false; // opened from the phone's More sheet
|
|
17
|
+
if (!dialog.open) dialog.showModal();
|
|
18
|
+
input.focus();
|
|
19
|
+
input.select();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
document.addEventListener("keydown", function (event) {
|
|
23
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") open(event);
|
|
24
|
+
});
|
|
25
|
+
document.querySelectorAll("[data-hrl-search-open]").forEach(function (link) {
|
|
26
|
+
link.addEventListener("click", open);
|
|
27
|
+
});
|
|
28
|
+
dialog.addEventListener("click", function (event) { if (event.target === dialog) dialog.close(); });
|
|
29
|
+
|
|
30
|
+
input.addEventListener("input", function () {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
timer = setTimeout(function () {
|
|
33
|
+
fetch(form.action + "?frame=1&q=" + encodeURIComponent(input.value), { credentials: "same-origin", cache: "no-store" })
|
|
34
|
+
.then(function (response) { return response.ok ? response.text() : ""; })
|
|
35
|
+
.then(function (html) { out.innerHTML = html; });
|
|
36
|
+
}, 180);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
dialog.addEventListener("keydown", function (event) {
|
|
40
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
|
|
41
|
+
var links = Array.prototype.slice.call(out.querySelectorAll("a"));
|
|
42
|
+
if (!links.length) return;
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
var at = links.indexOf(document.activeElement);
|
|
45
|
+
at = event.key === "ArrowDown" ? Math.min(at + 1, links.length - 1) : at - 1;
|
|
46
|
+
(at < 0 ? input : links[at]).focus();
|
|
47
|
+
});
|
|
48
|
+
})();
|
|
@@ -638,3 +638,8 @@ a.hrl-tree__name { display: inline-flex; align-items: center; min-height: var(--
|
|
|
638
638
|
.hrl-person { width: var(--hrl-target); height: var(--hrl-target); border-radius: var(--hrl-radius-pill); border: 2px solid var(--hrl-line); background: var(--hrl-bg); color: var(--hrl-ink); font-size: var(--hrl-t-small); font-weight: 600; cursor: pointer; }
|
|
639
639
|
.hrl-person--ok { border-color: var(--hrl-ok); background: var(--hrl-ok-bg); color: var(--hrl-ok); }
|
|
640
640
|
.hrl-person--warn { border-color: var(--hrl-warn); background: var(--hrl-warn-bg); color: var(--hrl-warn); }
|
|
641
|
+
|
|
642
|
+
/* Search palette (search.js): one box for screens and people, ⌘K anywhere. */
|
|
643
|
+
.hrl-search { width: min(36rem, calc(100% - 2rem)); margin: 10vh auto auto; padding: var(--hrl-s-4); border: 1px solid var(--hrl-line); border-radius: var(--hrl-radius); background: var(--hrl-card); color: var(--hrl-ink); }
|
|
644
|
+
.hrl-search::backdrop { background: rgb(15 23 42 / .4); }
|
|
645
|
+
.hrl-search__list { list-style: none; margin: var(--hrl-s-1) 0 0; padding: 0; display: grid; gap: var(--hrl-s-2); }
|
|
@@ -125,6 +125,34 @@ module HrLite
|
|
|
125
125
|
end
|
|
126
126
|
end
|
|
127
127
|
|
|
128
|
+
# One screen per person for a day, week (default) or month: each day, leave,
|
|
129
|
+
# penalties and every request, each linking to where it is fixed or decided.
|
|
130
|
+
def summary
|
|
131
|
+
@employee = find_visible_employee
|
|
132
|
+
@period = WorkModeDurations::Period.build(params[:period].presence_in(%w[day week month]) || "week", parse_date_param(params[:date]))
|
|
133
|
+
range = @period.from..@period.to
|
|
134
|
+
@days = DayStatus.new(user: @employee, range: range)
|
|
135
|
+
@punches = AttendancePunch.where(user_id: @employee.id, date: range).order(:at, :id).group_by(&:date)
|
|
136
|
+
@penalties = AttendancePenalty.active.where(user_id: @employee.id, date: range).group(:date).sum(:minutes)
|
|
137
|
+
@balances = LeaveType.active.where(paid: true).map { |type| LeaveBalance.for(@employee, type, LeaveYear.current_key) }
|
|
138
|
+
@requests = summary_requests(@employee, range)
|
|
139
|
+
@profile = EmployeeProfile.find_by(user_id: @employee.id)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The shorter day and its repeat, from This week: the employee form's own
|
|
143
|
+
# gate (profile.manage, company-wide) and never your own record.
|
|
144
|
+
def shorter_day
|
|
145
|
+
return hr_access_denied unless hr_can?("profile.manage", scope: :all)
|
|
146
|
+
@employee = find_visible_employee
|
|
147
|
+
raise ActiveRecord::RecordNotFound if @employee.id == hr_current_user.id
|
|
148
|
+
|
|
149
|
+
profile = EmployeeProfile.find_by!(user_id: @employee.id)
|
|
150
|
+
saved = profile.update(params.require(:employee_profile).permit(:hours_allowance_minutes, :hours_allowance_reason)
|
|
151
|
+
.merge(hours_allowance_repeat: RepeatRule.from_params(params.fetch(:repeat, {}))&.to_s))
|
|
152
|
+
redirect_to summary_admin_attendance_path(@employee.id, anchor: "shorter-day"),
|
|
153
|
+
saved ? { notice: "Shorter day saved." } : { alert: profile.errors.full_messages.to_sentence }
|
|
154
|
+
end
|
|
155
|
+
|
|
128
156
|
# HR sets a locked slot from today for the period.
|
|
129
157
|
def slot
|
|
130
158
|
@employee = find_manageable_employee
|
|
@@ -175,6 +203,32 @@ module HrLite
|
|
|
175
203
|
|
|
176
204
|
private
|
|
177
205
|
|
|
206
|
+
# [what, when, status, link] per request in the range, only for the types
|
|
207
|
+
# whose own page this viewer may open for this person.
|
|
208
|
+
def summary_requests(user, range)
|
|
209
|
+
id, from, to = user.id, range.first, range.last
|
|
210
|
+
day = ->(d) { d.strftime("%a %-d %b") }
|
|
211
|
+
{
|
|
212
|
+
"leave.view" => -> {
|
|
213
|
+
LeaveRequest.where(user_id: id).overlapping_range(from, to).includes(:leave_type)
|
|
214
|
+
.map { |r| [ r.leave_type.name, r.date_range_label, r.status, admin_leave_request_path(r) ] } +
|
|
215
|
+
CompOffRequest.where(user_id: id, date_worked: range).map { |r| [ "Comp-off", day.(r.date_worked), r.status, admin_comp_off_request_path(r) ] }
|
|
216
|
+
},
|
|
217
|
+
"attendance.view" => -> {
|
|
218
|
+
WfhRequest.where(user_id: id).overlapping(from, to).map { |r| [ "WFH", r.dates_label, r.status, admin_wfh_request_path(r) ] } +
|
|
219
|
+
RegularizationRequest.where(user_id: id, date: range).map { |r| [ "Attendance fix", day.(r.date), r.status, admin_regularization_request_path(r) ] }
|
|
220
|
+
},
|
|
221
|
+
"expense.approve" => -> {
|
|
222
|
+
Expense.where(user_id: id, spent_on: range).includes(:category)
|
|
223
|
+
.map { |r| [ "Claim · #{r.category.name}", day.(r.spent_on), r.status, admin_expenses_path ] }
|
|
224
|
+
},
|
|
225
|
+
"document.view" => -> {
|
|
226
|
+
Document.where(user_id: id, created_at: from.beginning_of_day..to.end_of_day)
|
|
227
|
+
.map { |r| [ "Document · #{r.title}", day.(r.created_at.to_date), r.verification, admin_employee_documents_path(id) ] }
|
|
228
|
+
}
|
|
229
|
+
}.select { |perm, _| hr_reaches?(perm, user) }.flat_map { |_, rows| rows.call }
|
|
230
|
+
end
|
|
231
|
+
|
|
178
232
|
# Viewing somebody's month and rewriting their punches are different
|
|
179
233
|
# authorities, so they resolve through different permissions — a role
|
|
180
234
|
# can be given the board without being given the pencil.
|
|
@@ -125,8 +125,11 @@ module HrLite
|
|
|
125
125
|
# hands one person's PAN, bank details and pay history to another, so it
|
|
126
126
|
# is a create-time field only.
|
|
127
127
|
def update_profile_params
|
|
128
|
-
profile_params.except(:user_id)
|
|
128
|
+
profile_params.except(:user_id).merge(repeat_param)
|
|
129
129
|
end
|
|
130
|
+
|
|
131
|
+
# The shorter day's repeat (RepeatRule); absent when the form has no repeat fields.
|
|
132
|
+
def repeat_param = params.key?(:repeat) ? { hours_allowance_repeat: RepeatRule.from_params(params[:repeat])&.to_s } : {}
|
|
130
133
|
end
|
|
131
134
|
end
|
|
132
135
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# One box for the whole HRMS: the screens this person may open (the menu's
|
|
3
|
+
# own gates) and the people they may see. Nothing outside their reach is
|
|
4
|
+
# ever listed; a page they still try to open answers as it always does.
|
|
5
|
+
class SearchController < ApplicationController
|
|
6
|
+
MIN_CHARS = 2
|
|
7
|
+
# Words people type that are in neither a tab's label nor its paths (which also match).
|
|
8
|
+
WORDS = {
|
|
9
|
+
"My attendance" => "check in check out punch penalty penalties regularization fix late",
|
|
10
|
+
"My leave" => "apply leave holiday comp-off balance", "Calendar" => "holidays",
|
|
11
|
+
"WFH" => "work from home", "Home locations" => "wfh location", "Slips" => "payslip salary slip",
|
|
12
|
+
"Expenses" => "claim reimbursement", "Employees" => "people staff", "Profile" => "my details",
|
|
13
|
+
"Settings" => "working day half day penalties leave type check in again"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
rate_limit to: 60, within: 1.minute, by: -> { hr_current_user.id }, name: "hr_search"
|
|
17
|
+
|
|
18
|
+
def show
|
|
19
|
+
@q = params[:q].to_s.strip
|
|
20
|
+
words = @q.downcase.split
|
|
21
|
+
limit = params[:frame] ? 5 : 10
|
|
22
|
+
@features = @people = []
|
|
23
|
+
if @q.length >= MIN_CHARS
|
|
24
|
+
@features = helpers.hrl_sections.flat_map { |s| s[:tabs].map { |t| [ "#{s[:label]} · #{t[:label]}", public_send(t[:path]), "#{s[:label]} #{t[:label]} #{WORDS[t[:label]]} #{Array(t[:match]).join(' ').tr('/_', ' ')}".downcase ] } }
|
|
25
|
+
.select { |_, _, text| words.all? { |w| text.include?(w) } }.first(limit)
|
|
26
|
+
@people = people_matching(words).first(limit) if hr_operations?
|
|
27
|
+
end
|
|
28
|
+
params[:frame] ? render(partial: "results") : render(:show)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Only people whose attendance this viewer may open; the result opens their This week.
|
|
34
|
+
def people_matching(words)
|
|
35
|
+
codes = EmployeeProfile.pluck(:user_id, :employee_code).to_h
|
|
36
|
+
HrLite.employees.select do |user|
|
|
37
|
+
text = [ HrLite.display_name(user), user.email, codes[user.id] ].join(" ").downcase
|
|
38
|
+
words.all? { |w| text.include?(w) } && hr_reaches?("attendance.view", user)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -170,7 +170,8 @@ module HrLite
|
|
|
170
170
|
"Me" => '<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>',
|
|
171
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
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"/>'
|
|
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
|
+
"Search" => '<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/>'
|
|
174
175
|
}.freeze
|
|
175
176
|
|
|
176
177
|
def hrl_icon(name)
|
|
@@ -54,7 +54,7 @@ module HrLite
|
|
|
54
54
|
# The working day HR sets in Settings, less the person's allowance. Nil when
|
|
55
55
|
# the host turns hours rules off (config.work_hours), and on Saturdays.
|
|
56
56
|
def required_minutes
|
|
57
|
-
EmployeeProfile.day_minutes_for(user_id) unless HrLite.config.work_hours.nil? || date.saturday?
|
|
57
|
+
EmployeeProfile.day_minutes_for(user_id, on: date) unless HrLite.config.work_hours.nil? || date.saturday?
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
# Hours worked, not the clock: an early start counts.
|
|
@@ -71,7 +71,7 @@ module HrLite
|
|
|
71
71
|
return unless HrLite.config.attendance_slots && check_in_at
|
|
72
72
|
|
|
73
73
|
extra = AttendancePenalty.active.where(user_id: user_id, date: date).sum(:minutes)
|
|
74
|
-
check_in_at + (EmployeeProfile.day_minutes_for(user_id) + extra).minutes
|
|
74
|
+
check_in_at + (EmployeeProfile.day_minutes_for(user_id, on: date) + extra).minutes
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
def worked_under?(minutes) = !!(minutes && worked_duration && worked_duration < minutes.minutes)
|
|
@@ -41,8 +41,13 @@ module HrLite
|
|
|
41
41
|
validates :hours_allowance_minutes, inclusion: { in: HOURS_ALLOWANCE_MINUTES }, allow_nil: true
|
|
42
42
|
validates :hours_allowance_reason, presence: true, if: :hours_allowance_minutes
|
|
43
43
|
|
|
44
|
-
# One person's working day: HR's day in Settings less their
|
|
45
|
-
|
|
44
|
+
# One person's working day on a date: HR's day in Settings less their
|
|
45
|
+
# allowance, on the days its repeat covers (no repeat = every day).
|
|
46
|
+
def self.day_minutes_for(user_id, on: Date.current)
|
|
47
|
+
minutes, repeat = where(user_id: user_id).pick(:hours_allowance_minutes, :hours_allowance_repeat)
|
|
48
|
+
applies = minutes && (repeat.blank? || RepeatRule.parse(repeat).occurs_on?(on))
|
|
49
|
+
Setting.instance.day_minutes - (applies ? minutes : 0)
|
|
50
|
+
end
|
|
46
51
|
|
|
47
52
|
TAX_REGIMES = %w[new old].freeze
|
|
48
53
|
BLOOD_GROUPS = %w[A+ A- B+ B- AB+ AB- O+ O-].freeze
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module HrLite
|
|
2
|
+
# A calendar repeat, like Google Calendar's custom repeat (an RRULE subset):
|
|
3
|
+
# every N days, weeks (on chosen weekdays) or months (on the start's date),
|
|
4
|
+
# from a start date, ending never, on a date, or after N times. Stored as
|
|
5
|
+
# text, e.g. "FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,WE,FR;DTSTART=2026-10-20;COUNT=12".
|
|
6
|
+
class RepeatRule
|
|
7
|
+
FREQS = { "DAILY" => "day", "WEEKLY" => "week", "MONTHLY" => "month" }.freeze
|
|
8
|
+
DAYS = %w[SU MO TU WE TH FR SA].freeze # index = Date#wday
|
|
9
|
+
|
|
10
|
+
attr_reader :freq, :interval, :days, :from, :until_date, :count
|
|
11
|
+
|
|
12
|
+
def self.parse(text)
|
|
13
|
+
return if text.blank?
|
|
14
|
+
|
|
15
|
+
parts = text.split(";").to_h { |part| part.split("=", 2) }
|
|
16
|
+
new(freq: parts["FREQ"], interval: parts["INTERVAL"], days: parts["BYDAY"].to_s.split(","),
|
|
17
|
+
from: parts["DTSTART"], until_date: parts["UNTIL"], count: parts["COUNT"])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The form's repeat[...] fields; a blank frequency means every day (no rule).
|
|
21
|
+
def self.from_params(params)
|
|
22
|
+
return if params[:freq].blank?
|
|
23
|
+
|
|
24
|
+
new(freq: params[:freq], interval: params[:interval], days: Array(params[:days]).compact_blank, from: params[:from],
|
|
25
|
+
until_date: (params[:until] if params[:ends] == "on"), count: (params[:count] if params[:ends] == "after"))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(freq:, interval: 1, days: [], from: nil, until_date: nil, count: nil)
|
|
29
|
+
@freq = FREQS.key?(freq.to_s.upcase) ? freq.to_s.upcase : "DAILY"
|
|
30
|
+
@interval = [ interval.to_i, 1 ].max
|
|
31
|
+
@days = days.map(&:upcase) & DAYS
|
|
32
|
+
@from = to_date(from) || Date.current
|
|
33
|
+
@until_date = to_date(until_date)
|
|
34
|
+
@count = count.to_i.positive? ? count.to_i : nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_s
|
|
38
|
+
[ "FREQ=#{freq}", "INTERVAL=#{interval}", ("BYDAY=#{days.join(',')}" if days.any?), "DTSTART=#{from.iso8601}",
|
|
39
|
+
("UNTIL=#{until_date.iso8601}" if until_date), ("COUNT=#{count}" if count) ].compact.join(";")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def occurs_on?(date)
|
|
43
|
+
return false if date < from || (until_date && date > until_date) || !matches?(date)
|
|
44
|
+
|
|
45
|
+
count.nil? || (from..date).count { |day| matches?(day) } <= count
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# "Every 2 weeks on Mon, Wed, Fri from 20 Oct 2026, 12 times"
|
|
49
|
+
def describe
|
|
50
|
+
every = interval == 1 ? "Every #{FREQS[freq]}" : "Every #{interval} #{FREQS[freq]}s"
|
|
51
|
+
on = (" on #{days.map { |d| Date::ABBR_DAYNAMES[DAYS.index(d)] }.join(', ')}" if freq == "WEEKLY" && days.any?)
|
|
52
|
+
ends = until_date ? ", until #{until_date.strftime('%-d %b %Y')}" : (", #{count} times" if count)
|
|
53
|
+
"#{every}#{on} from #{from.strftime('%-d %b %Y')}#{ends}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def matches?(date)
|
|
59
|
+
case freq
|
|
60
|
+
when "DAILY" then ((date - from).to_i % interval).zero?
|
|
61
|
+
when "WEEKLY"
|
|
62
|
+
(days.any? ? days.include?(DAYS[date.wday]) : date.wday == from.wday) &&
|
|
63
|
+
(((date.beginning_of_week - from.beginning_of_week).to_i / 7) % interval).zero?
|
|
64
|
+
else date.day == from.day && (((date.year * 12) + date.month - (from.year * 12) - from.month) % interval).zero?
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def to_date(value)
|
|
69
|
+
value.is_a?(Date) ? value : Date.iso8601(value.to_s)
|
|
70
|
+
rescue Date::Error
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<% content_for(:page_title) { "#{hr_display_name(@employee)} — #{@period.label}" } %>
|
|
2
|
+
<% content_for(:tabrow) { render "hr_lite/shared/person_tabs", user: @employee } %>
|
|
3
|
+
<% at = ->(args) { hr_lite.summary_admin_attendance_path(@employee.id, args) }
|
|
4
|
+
fix = @employee.id != hr_current_user.id && hr_reaches?("attendance.manage", @employee)
|
|
5
|
+
tones = { present: "hrl-badge--ok", half_day: "hrl-badge--warn", absent: "hrl-badge--bad" } %>
|
|
6
|
+
<div class="hrl-page__head">
|
|
7
|
+
<h1 class="hrl-page__title"><%= hr_display_name(@employee) %></h1>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<section class="hrl-card">
|
|
11
|
+
<div class="hrl-row hrl-row--tabs hrl-row--mb">
|
|
12
|
+
<% %w[day week month].each do |kind| %>
|
|
13
|
+
<a class="hrl-btn <%= 'hrl-btn--primary' if @period.kind == kind %>" <%= 'aria-current="page"'.html_safe if @period.kind == kind %> href="<%= at.(period: kind, date: @period.from) %>"><%= kind.capitalize %></a>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="hrl-row">
|
|
17
|
+
<a class="hrl-btn" href="<%= at.(period: @period.kind, date: @period.shift(-1)) %>" aria-label="Previous <%= @period.kind %>">‹</a>
|
|
18
|
+
<strong><%= @period.label %></strong>
|
|
19
|
+
<a class="hrl-btn" href="<%= at.(period: @period.kind, date: @period.shift(1)) %>" aria-label="Next <%= @period.kind %>">›</a>
|
|
20
|
+
</div>
|
|
21
|
+
<%= form_with url: hr_lite.summary_admin_attendance_path(@employee.id), method: :get, local: true, class: "hrl-row hrl-mt" do %>
|
|
22
|
+
<input type="hidden" name="period" value="<%= @period.kind %>">
|
|
23
|
+
<div class="hrl-field"><label for="pw-date">Go to date</label><%= date_field_tag :date, @period.from, id: "pw-date" %></div>
|
|
24
|
+
<div class="hrl-form-actions"><button type="submit" class="hrl-btn">Show</button></div>
|
|
25
|
+
<% end %>
|
|
26
|
+
</section>
|
|
27
|
+
|
|
28
|
+
<section class="hrl-card">
|
|
29
|
+
<h2 class="hrl-card__title">Days</h2>
|
|
30
|
+
<% (@period.from..@period.to).each do |date| %>
|
|
31
|
+
<% day = @days.for(date); rec = day.record; punches = @punches[date].to_a %>
|
|
32
|
+
<div class="hrl-feed__item hrl-row">
|
|
33
|
+
<div class="hrl-feed__body hrl-small">
|
|
34
|
+
<strong><%= date.strftime("%a %-d %b") %></strong>
|
|
35
|
+
<span class="hrl-badge <%= tones[day.kind] %>"><%= HrLite::ApplicationHelper::DAY_STATES[day.kind][1] %></span>
|
|
36
|
+
<% if @profile&.hours_allowance_minutes && (length = HrLite::EmployeeProfile.day_minutes_for(@employee.id, on: date)) < HrLite::Setting.instance.day_minutes %>
|
|
37
|
+
<span class="hrl-badge hrl-badge--muted">Day <%= hrl_duration(length * 60) %></span>
|
|
38
|
+
<% end %>
|
|
39
|
+
<% if rec&.check_in_at %>
|
|
40
|
+
<div>In <%= rec.check_in_at.strftime("%H:%M") %><% if rec.check_out_at %> · Out <%= rec.check_out_at.strftime("%H:%M") %> · <%= hrl_duration(rec.worked_duration) %> worked<% end %></div>
|
|
41
|
+
<% end %>
|
|
42
|
+
<% if punches.size > 2 %><div>All punches: <%= punches.join(" · ") %></div><% end %>
|
|
43
|
+
<% if (minutes = @penalties[date]) %><div><span class="hrl-badge hrl-badge--warn">+<%= minutes %> min</span> penalty</div><% end %>
|
|
44
|
+
<% if rec&.flagged? %><div>Flagged: <%= rec.flag_note %></div><% end %>
|
|
45
|
+
</div>
|
|
46
|
+
<% if fix && date <= Date.current %>
|
|
47
|
+
<a class="hrl-btn" href="<%= hr_lite.admin_attendance_path(@employee.id, date: date, month: date.strftime("%Y-%m"), anchor: "regularize") %>">Fix<span class="hrl-sr-only"> <%= date.strftime("%-d %b") %></span></a>
|
|
48
|
+
<% end %>
|
|
49
|
+
</div>
|
|
50
|
+
<% end %>
|
|
51
|
+
</section>
|
|
52
|
+
|
|
53
|
+
<% if @profile %>
|
|
54
|
+
<section class="hrl-card" id="shorter-day">
|
|
55
|
+
<h2 class="hrl-card__title">Shorter day</h2>
|
|
56
|
+
<p class="hrl-small"><% if @profile.hours_allowance_minutes %><strong><%= @profile.hours_allowance_minutes %> min less</strong> · <%= HrLite::RepeatRule.parse(@profile.hours_allowance_repeat)&.describe || "Every day" %><% else %>None<% end %></p>
|
|
57
|
+
<% if @employee.id != hr_current_user.id && hr_can?("profile.manage", scope: :all) %>
|
|
58
|
+
<% if @profile.hours_allowance_reason.present? %><p class="hrl-small hrl-muted">Reason: <%= @profile.hours_allowance_reason %></p><% end %>
|
|
59
|
+
<details>
|
|
60
|
+
<summary class="hrl-btn hrl-btn--block"><%= @profile.hours_allowance_minutes ? "Change shorter day" : "Set a shorter day" %></summary>
|
|
61
|
+
<%= form_with model: @profile, scope: :employee_profile, url: hr_lite.shorter_day_admin_attendance_path(@employee.id), method: :patch, local: true do |f| %>
|
|
62
|
+
<%= render "hr_lite/admin/employees/shorter_day_fields", f: f, profile: @profile %>
|
|
63
|
+
<button type="submit" class="hrl-btn hrl-btn--primary">Save shorter day</button>
|
|
64
|
+
<% end %>
|
|
65
|
+
</details>
|
|
66
|
+
<% end %>
|
|
67
|
+
</section>
|
|
68
|
+
<% end %>
|
|
69
|
+
|
|
70
|
+
<section class="hrl-card">
|
|
71
|
+
<h2 class="hrl-card__title">Requests</h2>
|
|
72
|
+
<% if @requests.empty? %><p class="hrl-card--empty">No requests in this <%= @period.kind %>.</p><% end %>
|
|
73
|
+
<% @requests.each do |what, dates, status, url| %>
|
|
74
|
+
<div class="hrl-feed__item hrl-row">
|
|
75
|
+
<div class="hrl-feed__body hrl-small"><strong><%= what %></strong> · <%= dates %> <%= hrl_request_status_badge(status) %></div>
|
|
76
|
+
<a class="hrl-btn" href="<%= url %>">Open<span class="hrl-sr-only"> <%= what %>, <%= dates %></span></a>
|
|
77
|
+
</div>
|
|
78
|
+
<% end %>
|
|
79
|
+
</section>
|
|
80
|
+
|
|
81
|
+
<% if hr_reaches?("leave.view", @employee) %>
|
|
82
|
+
<section class="hrl-card">
|
|
83
|
+
<h2 class="hrl-card__title">Leave balance</h2>
|
|
84
|
+
<% @balances.each do |balance| %>
|
|
85
|
+
<div class="hrl-feed__item hrl-small"><strong><%= balance.leave_type.name %></strong> · <%= hrl_days(balance.available) %> left
|
|
86
|
+
<% if (held = balance.penalty_days).positive? %> · <%= hrl_days(held) %> held for penalties · can apply <%= hrl_days(balance.applicable) %><% end %></div>
|
|
87
|
+
<% end %>
|
|
88
|
+
</section>
|
|
89
|
+
<% end %>
|
|
90
|
+
|
|
91
|
+
<% if HrLite.config.attendance_slots %>
|
|
92
|
+
<%= render "hr_lite/attendance/penalties", user: @employee, month: @period.from, removable: fix %>
|
|
93
|
+
<% end %>
|
|
@@ -90,11 +90,7 @@
|
|
|
90
90
|
</div>
|
|
91
91
|
<div class="hrl-field"><%= f.label :buffer_note, "Buffer note (HR only; required for Other)" %><%= f.text_area :buffer_note, rows: 2 %></div>
|
|
92
92
|
<% end %>
|
|
93
|
-
|
|
94
|
-
<%= f.label :hours_allowance_minutes, "Shorter day (e.g. a long commute; lowers the hours they must work)" %>
|
|
95
|
-
<%= f.select :hours_allowance_minutes, HrLite::EmployeeProfile::HOURS_ALLOWANCE_MINUTES.map { |m| [ "#{m} min less", m ] }, include_blank: "None" %>
|
|
96
|
-
</div>
|
|
97
|
-
<div class="hrl-field"><%= f.label :hours_allowance_reason, "Shorter day reason (HR only; required)" %><%= f.text_area :hours_allowance_reason, rows: 2 %></div>
|
|
93
|
+
<%= render "hr_lite/admin/employees/shorter_day_fields", f: f, profile: f.object %>
|
|
98
94
|
<% if HrLite.config.office_geofence %>
|
|
99
95
|
<div class="hrl-field hrl-field--check"><%= f.check_box :permanent_wfh %><%= f.label :permanent_wfh, "Permanent work from home (every working day counts as approved WFH; check-in still within 500 m of their approved home location)" %></div>
|
|
100
96
|
<div class="hrl-field hrl-field--check"><%= f.check_box :field_staff %><%= f.label :field_staff, "Field staff (e.g. sales: may check in from anywhere as Travelling or Client meeting, with purpose and place)" %></div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<%# locals: f (employee_profile form), profile — the shorter day and the days it repeats on (repeat[...] → RepeatRule). %>
|
|
2
|
+
<% rule = HrLite::RepeatRule.parse(profile.hours_allowance_repeat)
|
|
3
|
+
ends = rule&.until_date ? "on" : (rule&.count ? "after" : "never") %>
|
|
4
|
+
<div class="hrl-field">
|
|
5
|
+
<%= f.label :hours_allowance_minutes, "Shorter day (e.g. a long commute; lowers the hours they must work)" %>
|
|
6
|
+
<%= f.select :hours_allowance_minutes, HrLite::EmployeeProfile::HOURS_ALLOWANCE_MINUTES.map { |m| [ "#{m} min less", m ] }, include_blank: "None" %>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="hrl-field">
|
|
9
|
+
<label for="repeat-freq">Repeats</label>
|
|
10
|
+
<%= select_tag "repeat[freq]", options_for_select([ [ "Every day", "" ], [ "Every N days", "DAILY" ], [ "Every N weeks, on the days below", "WEEKLY" ], [ "Every N months, on the start's date", "MONTHLY" ] ], rule&.freq), id: "repeat-freq" %>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="hrl-field"><label for="repeat-interval">N (every how many)</label><%= number_field_tag "repeat[interval]", rule&.interval || 1, min: 1, max: 12, id: "repeat-interval", class: "hrl-input" %></div>
|
|
13
|
+
<fieldset class="hrl-field">
|
|
14
|
+
<legend>Days (weekly)</legend>
|
|
15
|
+
<div class="hrl-row">
|
|
16
|
+
<% HrLite::RepeatRule::DAYS.rotate(1).each do |day| %>
|
|
17
|
+
<label class="hrl-field--check"><%= check_box_tag "repeat[days][]", day, rule&.days&.include?(day), id: "repeat-#{day}" %> <%= Date::ABBR_DAYNAMES[HrLite::RepeatRule::DAYS.index(day)] %></label>
|
|
18
|
+
<% end %>
|
|
19
|
+
</div>
|
|
20
|
+
</fieldset>
|
|
21
|
+
<div class="hrl-field"><label for="repeat-from">Starts</label><%= date_field_tag "repeat[from]", rule&.from || Date.current, id: "repeat-from" %></div>
|
|
22
|
+
<fieldset class="hrl-field">
|
|
23
|
+
<legend>Ends</legend>
|
|
24
|
+
<label class="hrl-field--check"><%= radio_button_tag "repeat[ends]", "never", ends == "never", id: "repeat-ends-never" %> Never</label>
|
|
25
|
+
<label class="hrl-field--check"><%= radio_button_tag "repeat[ends]", "on", ends == "on", id: "repeat-ends-on" %> On a date</label>
|
|
26
|
+
<label class="hrl-field--check"><%= radio_button_tag "repeat[ends]", "after", ends == "after", id: "repeat-ends-after" %> After a number of times</label>
|
|
27
|
+
</fieldset>
|
|
28
|
+
<div class="hrl-field"><label for="repeat-until">End date (for "On a date")</label><%= date_field_tag "repeat[until]", rule&.until_date, id: "repeat-until" %></div>
|
|
29
|
+
<div class="hrl-field"><label for="repeat-count">Times (for "After a number of times")</label><%= number_field_tag "repeat[count]", rule&.count, min: 1, id: "repeat-count", class: "hrl-input" %></div>
|
|
30
|
+
<div class="hrl-field"><%= f.label :hours_allowance_reason, "Shorter day reason (HR only; required)" %><%= f.text_area :hours_allowance_reason, rows: 2 %></div>
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
<% if record&.work_mode_label %>
|
|
24
24
|
<span class="hrl-badge hrl-badge--ok"><%= record.work_mode_label %></span>
|
|
25
25
|
<% end %>
|
|
26
|
-
<% if HrLite::EmployeeProfile.
|
|
27
|
-
<span class="hrl-badge hrl-badge--muted">Your day <%= hrl_duration(
|
|
26
|
+
<% if (my_day = HrLite::EmployeeProfile.day_minutes_for(hr_current_user.id)) < HrLite::Setting.instance.day_minutes %>
|
|
27
|
+
<span class="hrl-badge hrl-badge--muted">Your day <%= hrl_duration(my_day * 60) %></span>
|
|
28
28
|
<% end %>
|
|
29
29
|
<% if record&.check_in_place == "home" && record.work_location %>
|
|
30
30
|
<span class="hrl-badge hrl-badge--muted"><%= record.work_location.name %> · <%= HrLite::WorkLocation::FENCE_M %> m</span>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<% if @q.length < HrLite::SearchController::MIN_CHARS %>
|
|
2
|
+
<p class="hrl-small hrl-muted">Type at least <%= HrLite::SearchController::MIN_CHARS %> letters — a screen (payslip, WFH, penalty) or a person.</p>
|
|
3
|
+
<% elsif @features.empty? && @people.empty? %>
|
|
4
|
+
<p class="hrl-small hrl-muted">Nothing you can open matches “<%= @q %>”.</p>
|
|
5
|
+
<% else %>
|
|
6
|
+
<% [ [ "Screens", @features.map { |label, url, _| [ label, url ] } ],
|
|
7
|
+
[ "People", @people.map { |user| [ hr_display_name(user), hr_lite.summary_admin_attendance_path(user.id) ] } ] ].each do |title, rows| %>
|
|
8
|
+
<% next if rows.empty? %>
|
|
9
|
+
<h2 class="hrl-small hrl-muted hrl-mt"><%= title %></h2>
|
|
10
|
+
<ul class="hrl-search__list">
|
|
11
|
+
<% rows.each do |label, url| %><li><a class="hrl-nav__link" href="<%= url %>"><%= label %></a></li><% end %>
|
|
12
|
+
</ul>
|
|
13
|
+
<% end %>
|
|
14
|
+
<% end %>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Search" } %>
|
|
2
|
+
<div class="hrl-page__head"><h1 class="hrl-page__title">Search</h1></div>
|
|
3
|
+
<section class="hrl-card">
|
|
4
|
+
<form action="<%= hr_lite.search_path %>" method="get" role="search" class="hrl-field">
|
|
5
|
+
<label for="search-page-q">Search screens and people</label>
|
|
6
|
+
<input id="search-page-q" class="hrl-input" type="search" name="q" value="<%= @q %>" autocomplete="off" autofocus>
|
|
7
|
+
</form>
|
|
8
|
+
<%= render "results" %>
|
|
9
|
+
</section>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<% ops = hr_operations?
|
|
3
3
|
profile = HrLite::EmployeeProfile.find_by(user_id: user.id)
|
|
4
4
|
tabs = [
|
|
5
|
+
[ "This week", (hr_lite.summary_admin_attendance_path(user.id) if ops && hr_reaches?("attendance.view", user)) ],
|
|
5
6
|
[ "Overview", (hr_lite.admin_employee_path(profile) if profile && hr_leadership?) ],
|
|
6
7
|
[ "Attendance", (hr_lite.admin_attendance_path(user.id) if ops && hr_reaches?("attendance.view", user)) ],
|
|
7
8
|
[ "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)) ],
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
</a>
|
|
27
27
|
|
|
28
28
|
<nav class="hrl-side__nav">
|
|
29
|
+
<a class="hrl-nav__link" href="<%= hr_lite.search_path %>" data-hrl-search-open><%= hrl_icon("Search") %>Search<span class="hrl-sr-only"> (Ctrl K)</span></a>
|
|
29
30
|
<% hrl_sections.each do |section| %>
|
|
30
31
|
<% if section[:divider] %><span class="hrl-side__divider" aria-hidden="true"></span><% end %>
|
|
31
32
|
<%= hrl_nav_link(section) %>
|
|
@@ -94,6 +95,7 @@
|
|
|
94
95
|
<% if HrLite.config.back_link %>
|
|
95
96
|
<a class="hrl-nav__link hrl-side__back" href="<%= HrLite.config.back_link[:url] %>"><%= HrLite.config.back_link[:label] %></a>
|
|
96
97
|
<% end %>
|
|
98
|
+
<a class="hrl-nav__link" href="<%= hr_lite.search_path %>" data-hrl-search-open><%= hrl_icon("Search") %>Search<span class="hrl-sr-only"> (Ctrl K)</span></a>
|
|
97
99
|
<%# The second line names the place's tabs, so a phone user knows what is inside before opening it. %>
|
|
98
100
|
<% rest.each do |section| %>
|
|
99
101
|
<%= hrl_nav_link(section, hint: section[:tabs].map { |t| t[:label] }.join(", ")) %>
|
|
@@ -105,7 +107,15 @@
|
|
|
105
107
|
<%= javascript_include_tag "hr_lite/confirm", defer: true %>
|
|
106
108
|
<%= javascript_include_tag "hr_lite/mention", defer: true %>
|
|
107
109
|
<%= javascript_include_tag "hr_lite/nav", defer: true %>
|
|
108
|
-
|
|
110
|
+
<%= javascript_include_tag "hr_lite/select_filter", defer: true %>
|
|
111
|
+
<%= javascript_include_tag "hr_lite/search", defer: true %>
|
|
112
|
+
<dialog class="hrl-search" data-hrl-search aria-label="Search">
|
|
113
|
+
<form action="<%= hr_lite.search_path %>" method="get" role="search">
|
|
114
|
+
<label class="hrl-sr-only" for="hrl-search-q">Search screens and people</label>
|
|
115
|
+
<input id="hrl-search-q" class="hrl-input" type="search" name="q" placeholder="Search screens and people" autocomplete="off">
|
|
116
|
+
</form>
|
|
117
|
+
<div data-hrl-search-results aria-live="polite"></div>
|
|
118
|
+
</dialog>
|
|
109
119
|
<%= yield :scripts %>
|
|
110
120
|
</body>
|
|
111
121
|
</html>
|
data/config/routes.rb
CHANGED
|
@@ -58,6 +58,7 @@ HrLite::Engine.routes.draw do
|
|
|
58
58
|
get "people/:user_id/timeline", to: "timelines#show", as: :person_timeline
|
|
59
59
|
resources :holidays, only: :index
|
|
60
60
|
get "calendar", to: "calendar#show"
|
|
61
|
+
get "search", to: "search#show", as: :search
|
|
61
62
|
resources :salary_slips, only: %i[index show]
|
|
62
63
|
resource :employee_profile, only: :show, path: "profile"
|
|
63
64
|
patch "id_card/photo", to: "id_cards#photo", as: :id_card_photo
|
|
@@ -73,7 +74,7 @@ HrLite::Engine.routes.draw do
|
|
|
73
74
|
get "overview", to: "overview#index"
|
|
74
75
|
resources :attendances, only: %i[index show update], param: :user_id do
|
|
75
76
|
collection { get :week }
|
|
76
|
-
member { post :slot; post :unlock_slot; post :remove_penalty; post :remove_penalty_hours }
|
|
77
|
+
member { get :summary; patch :shorter_day; post :slot; post :unlock_slot; post :remove_penalty; post :remove_penalty_hours }
|
|
77
78
|
end
|
|
78
79
|
resources :leave_requests, only: %i[index show new create edit update] do
|
|
79
80
|
member { post :approve; post :reject; post :cancel }
|
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.51.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kshitiz sinha
|
|
@@ -74,6 +74,7 @@ files:
|
|
|
74
74
|
- app/assets/javascripts/hr_lite/nav.js
|
|
75
75
|
- app/assets/javascripts/hr_lite/photo_crop.js
|
|
76
76
|
- app/assets/javascripts/hr_lite/print.js
|
|
77
|
+
- app/assets/javascripts/hr_lite/search.js
|
|
77
78
|
- app/assets/javascripts/hr_lite/select_filter.js
|
|
78
79
|
- app/assets/javascripts/hr_lite/share.js
|
|
79
80
|
- app/assets/javascripts/hr_lite/tax_preview.js
|
|
@@ -147,6 +148,7 @@ files:
|
|
|
147
148
|
- app/controllers/hr_lite/regularization_requests_controller.rb
|
|
148
149
|
- app/controllers/hr_lite/resignations_controller.rb
|
|
149
150
|
- app/controllers/hr_lite/salary_slips_controller.rb
|
|
151
|
+
- app/controllers/hr_lite/search_controller.rb
|
|
150
152
|
- app/controllers/hr_lite/tax_declarations_controller.rb
|
|
151
153
|
- app/controllers/hr_lite/team_controller.rb
|
|
152
154
|
- app/controllers/hr_lite/timelines_controller.rb
|
|
@@ -218,6 +220,7 @@ files:
|
|
|
218
220
|
- app/models/hr_lite/policy_acknowledgement.rb
|
|
219
221
|
- app/models/hr_lite/professional_tax_slab.rb
|
|
220
222
|
- app/models/hr_lite/regularization_request.rb
|
|
223
|
+
- app/models/hr_lite/repeat_rule.rb
|
|
221
224
|
- app/models/hr_lite/resignation.rb
|
|
222
225
|
- app/models/hr_lite/role.rb
|
|
223
226
|
- app/models/hr_lite/role_assignment.rb
|
|
@@ -263,6 +266,7 @@ files:
|
|
|
263
266
|
- app/views/hr_lite/admin/attendances/_slot.html.erb
|
|
264
267
|
- app/views/hr_lite/admin/attendances/index.html.erb
|
|
265
268
|
- app/views/hr_lite/admin/attendances/show.html.erb
|
|
269
|
+
- app/views/hr_lite/admin/attendances/summary.html.erb
|
|
266
270
|
- app/views/hr_lite/admin/attendances/week.html.erb
|
|
267
271
|
- app/views/hr_lite/admin/audit_logs/index.html.erb
|
|
268
272
|
- app/views/hr_lite/admin/awards/_form.html.erb
|
|
@@ -279,6 +283,7 @@ files:
|
|
|
279
283
|
- app/views/hr_lite/admin/documents/index.html.erb
|
|
280
284
|
- app/views/hr_lite/admin/documents/show.html.erb
|
|
281
285
|
- app/views/hr_lite/admin/employees/_form.html.erb
|
|
286
|
+
- app/views/hr_lite/admin/employees/_shorter_day_fields.html.erb
|
|
282
287
|
- app/views/hr_lite/admin/employees/edit.html.erb
|
|
283
288
|
- app/views/hr_lite/admin/employees/index.html.erb
|
|
284
289
|
- app/views/hr_lite/admin/employees/new.html.erb
|
|
@@ -400,6 +405,8 @@ files:
|
|
|
400
405
|
- app/views/hr_lite/salary_slips/index.html.erb
|
|
401
406
|
- app/views/hr_lite/salary_slips/pdf.html.erb
|
|
402
407
|
- app/views/hr_lite/salary_slips/show.html.erb
|
|
408
|
+
- app/views/hr_lite/search/_results.html.erb
|
|
409
|
+
- app/views/hr_lite/search/show.html.erb
|
|
403
410
|
- app/views/hr_lite/shared/_bell.html.erb
|
|
404
411
|
- app/views/hr_lite/shared/_form_errors.html.erb
|
|
405
412
|
- app/views/hr_lite/shared/_pagination.html.erb
|
|
@@ -494,6 +501,7 @@ files:
|
|
|
494
501
|
- db/migrate/20261015100000_create_hr_lite_attendance_punches.rb
|
|
495
502
|
- db/migrate/20261016090000_add_penalty_leave_type_to_hr_lite_settings.rb
|
|
496
503
|
- db/migrate/20261016091000_create_hr_lite_penalty_removals.rb
|
|
504
|
+
- db/migrate/20261016100000_add_hours_allowance_repeat_to_hr_lite_employee_profiles.rb
|
|
497
505
|
- lib/generators/hr_lite/install/install_generator.rb
|
|
498
506
|
- lib/generators/hr_lite/install/templates/AFTER_INSTALL
|
|
499
507
|
- lib/generators/hr_lite/install/templates/initializer.rb
|