hr_lite 0.50.0 → 0.52.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 +16 -0
- data/app/controllers/hr_lite/admin/attendances_controller.rb +15 -0
- data/app/controllers/hr_lite/admin/documents_controller.rb +25 -4
- data/app/controllers/hr_lite/admin/employees_controller.rb +4 -1
- data/app/controllers/hr_lite/documents_controller.rb +1 -1
- data/app/models/hr_lite/attendance_record.rb +6 -3
- 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 +20 -0
- data/app/views/hr_lite/admin/documents/show.html.erb +4 -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 +4 -2
- data/app/views/hr_lite/documents/_upload_form.html.erb +30 -0
- data/app/views/hr_lite/documents/index.html.erb +1 -29
- 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 +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6668e3a20cc3b84ebe77b9ae78ff2c20a12c37af21193d88b5f723a1383a7dca
|
|
4
|
+
data.tar.gz: ec7757fc4f4c0220dd21f05037b4054221a76ae445306c32911a2e71dcdd841d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 68367c0172ccdf649c52272c4c9256e00ecdd55452f7a3cbecd74a12f58e91966eed0b7e5b03f0d20a4380570a219e1602da4604e09cfa7584af20f78097f664
|
|
7
|
+
data.tar.gz: 6128e7796d5bc897db57b0c5ca1ac6a8dfb2ece9eadecf423f5d28815f1a9f847192845976893843b90cac4927cec61ea2c4221912a05f6d161238e944bb6c19
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.52.0] - 2026-10-16
|
|
11
|
+
|
|
12
|
+
- HR uploads documents for an employee: the person's Documents page (HR → People → a person → Documents) now has "Upload a document", the same form as My documents. Only company-wide profile managers (HR, admin) see it, and never on their own record. The document belongs to the employee, records who uploaded it, and its visibility still follows its category (an Aadhaar, PAN, passport or bank document stays with the money tier).
|
|
13
|
+
- Download links now expire after 5 minutes. Before, the file link behind "Open" never expired, so anyone holding a copied link could fetch the scan without signing in. Every download is still permission-checked and audited first.
|
|
14
|
+
|
|
15
|
+
## [0.51.1] - 2026-10-16
|
|
16
|
+
|
|
17
|
+
- No "Check in again" once the day is worked in full: after checking out with the person's day done (HR's 8h 30m, less any shorter day), the button is gone and the card reads "Your day is done: 8h 45m worked." The server refuses a check-in again too. A short day still offers it as before.
|
|
18
|
+
|
|
19
|
+
## [0.51.0] - 2026-10-16
|
|
20
|
+
|
|
21
|
+
- 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.
|
|
22
|
+
- 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.
|
|
23
|
+
- 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".
|
|
24
|
+
- Migration: `hr_lite_employee_profiles.hours_allowance_repeat` (string). Hosts rebuild their schema on the bump.
|
|
25
|
+
|
|
10
26
|
## [0.50.0] - 2026-10-16
|
|
11
27
|
|
|
12
28
|
- 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.
|
|
@@ -136,6 +136,21 @@ module HrLite
|
|
|
136
136
|
@penalties = AttendancePenalty.active.where(user_id: @employee.id, date: range).group(:date).sum(:minutes)
|
|
137
137
|
@balances = LeaveType.active.where(paid: true).map { |type| LeaveBalance.for(@employee, type, LeaveYear.current_key) }
|
|
138
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 }
|
|
139
154
|
end
|
|
140
155
|
|
|
141
156
|
# HR sets a locked slot from today for the period.
|
|
@@ -17,10 +17,24 @@ module HrLite
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def show
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
load_person
|
|
21
|
+
@document = Document.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# HR files a document for someone, e.g. a scan handed over on paper:
|
|
25
|
+
# company-wide profile managers only, and never their own (My documents).
|
|
26
|
+
def create
|
|
27
|
+
load_person
|
|
28
|
+
raise ActiveRecord::RecordNotFound if @employee.id == hr_current_user.id
|
|
29
|
+
return hr_access_denied unless hr_can?("profile.manage", scope: :all)
|
|
30
|
+
|
|
31
|
+
@document = Document.new(params.require(:document).permit(:category, :title, :reference_number, :issued_on, :expires_on, :file)
|
|
32
|
+
.merge(user_id: @employee.id, uploaded_by_id: hr_current_user.id))
|
|
33
|
+
if @document.save
|
|
34
|
+
redirect_to admin_employee_documents_path(@employee.id), notice: "#{@document.title} uploaded."
|
|
35
|
+
else
|
|
36
|
+
render :show, status: :unprocessable_entity
|
|
37
|
+
end
|
|
24
38
|
end
|
|
25
39
|
|
|
26
40
|
def verify
|
|
@@ -44,6 +58,13 @@ module HrLite
|
|
|
44
58
|
|
|
45
59
|
private
|
|
46
60
|
|
|
61
|
+
def load_person
|
|
62
|
+
@employee = HrLite.user_klass.find(params[:user_id])
|
|
63
|
+
hr_require_reach!("document.view", @employee)
|
|
64
|
+
@documents = Document.for_user(@employee).recent_first
|
|
65
|
+
@missing = Document::SENSITIVE_CATEGORIES - @documents.map(&:category)
|
|
66
|
+
end
|
|
67
|
+
|
|
47
68
|
def require_document_access!
|
|
48
69
|
hr_require_permission!("document.view", scope: :team)
|
|
49
70
|
end
|
|
@@ -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
|
|
@@ -56,7 +56,7 @@ module HrLite
|
|
|
56
56
|
# rails_blob_path, not url_for: an engine controller has no default
|
|
57
57
|
# host and url_for(attachment) raises without one.
|
|
58
58
|
redirect_to Rails.application.routes.url_helpers.rails_blob_path(
|
|
59
|
-
document.file, disposition: "attachment", only_path: true
|
|
59
|
+
document.file, disposition: "attachment", only_path: true, expires_in: 5.minutes # the link dies with the visit
|
|
60
60
|
), allow_other_host: false
|
|
61
61
|
end
|
|
62
62
|
|
|
@@ -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,13 +71,16 @@ 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)
|
|
78
78
|
|
|
79
|
-
# Check in again after checking out: once a day, inside the hours HR sets in
|
|
79
|
+
# Check in again after checking out: once a day, inside the hours HR sets in
|
|
80
|
+
# Settings, and only while the day is short (a full day has nothing to add).
|
|
80
81
|
def recheck_in_open?(at = Time.current)
|
|
82
|
+
return false if required_minutes && !short?
|
|
83
|
+
|
|
81
84
|
s = Setting.instance
|
|
82
85
|
now = at.in_time_zone(HrLite.config.time_zone).strftime("%H:%M")
|
|
83
86
|
check_out_at.present? && rechecked_in_at.nil? && now >= s.recheck_in_from && now < s.recheck_in_until
|
|
@@ -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
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
<div class="hrl-feed__body hrl-small">
|
|
34
34
|
<strong><%= date.strftime("%a %-d %b") %></strong>
|
|
35
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 %>
|
|
36
39
|
<% if rec&.check_in_at %>
|
|
37
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>
|
|
38
41
|
<% end %>
|
|
@@ -47,6 +50,23 @@
|
|
|
47
50
|
<% end %>
|
|
48
51
|
</section>
|
|
49
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
|
+
|
|
50
70
|
<section class="hrl-card">
|
|
51
71
|
<h2 class="hrl-card__title">Requests</h2>
|
|
52
72
|
<% if @requests.empty? %><p class="hrl-card--empty">No requests in this <%= @period.kind %>.</p><% end %>
|
|
@@ -41,3 +41,7 @@
|
|
|
41
41
|
</div>
|
|
42
42
|
<% end %>
|
|
43
43
|
</section>
|
|
44
|
+
|
|
45
|
+
<% if @employee.id != hr_current_user.id && hr_can?("profile.manage", scope: :all) %>
|
|
46
|
+
<%= render "hr_lite/documents/upload_form", document: @document, url: hr_lite.admin_employee_documents_path(@employee.id), open: @document.errors.any? %>
|
|
47
|
+
<% 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>
|
|
@@ -108,6 +108,8 @@
|
|
|
108
108
|
</button>
|
|
109
109
|
<p class="hrl-small hrl-muted" data-hrl-geo-status role="status" aria-live="polite"></p>
|
|
110
110
|
<% end %>
|
|
111
|
+
<% elsif record.required_minutes && !record.short? %>
|
|
112
|
+
<p class="hrl-small hrl-muted">Your day is done: <%= hrl_duration(record.worked_duration) %> worked.</p>
|
|
111
113
|
<% else %>
|
|
112
114
|
<p class="hrl-small hrl-muted">Check in again is open once a day, <%= HrLite::Setting.instance.recheck_in_from %>–<%= HrLite::Setting.instance.recheck_in_until %>.</p>
|
|
113
115
|
<% end %>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<%# locals: document, url (where it posts), open (start expanded) — one upload form for My documents and HR's view of a person. %>
|
|
2
|
+
<details class="hrl-card" <%= "open" if open || document.errors.any? %>>
|
|
3
|
+
<summary class="hrl-btn hrl-btn--primary">Upload a document</summary>
|
|
4
|
+
<%= form_with model: document, url: url, method: :post, local: true, class: "hrl-mt" do |f| %>
|
|
5
|
+
<%= render "hr_lite/shared/form_errors", record: document %>
|
|
6
|
+
<div class="hrl-field">
|
|
7
|
+
<%= f.label :category, id: "hrl-doc-category", for: nil %>
|
|
8
|
+
<div class="hrl-chiprow" role="radiogroup" aria-labelledby="hrl-doc-category">
|
|
9
|
+
<% (HrLite::Document::SENSITIVE_CATEGORIES.map { |c| [ c.humanize.sub(/\APan\z/, "PAN"), c ] } +
|
|
10
|
+
[ [ "Offer letter", "offer_letter" ], [ "Certificate", "certificate" ], [ "Other", "other" ] ]).each do |label, value| %>
|
|
11
|
+
<label><%= f.radio_button :category, value, required: true %><span><%= label %></span></label>
|
|
12
|
+
<% end %>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="hrl-field"><%= f.label :title %><%= f.text_field :title, required: true %></div>
|
|
16
|
+
<div class="hrl-field">
|
|
17
|
+
<%= f.label :reference_number, "Document number (optional)" %>
|
|
18
|
+
<%= f.text_field :reference_number %>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="hrl-field">
|
|
21
|
+
<%= f.label :expires_on, "Expires on (optional — we will remind you)" %>
|
|
22
|
+
<%= f.date_field :expires_on %>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="hrl-field">
|
|
25
|
+
<%= f.label :file, "File (PDF or image, up to 10 MB)" %>
|
|
26
|
+
<%= f.file_field :file, required: true, accept: "application/pdf,image/*" %>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="hrl-form-actions"><%= f.submit "Upload", class: "hrl-btn hrl-btn--primary" %></div>
|
|
29
|
+
<% end %>
|
|
30
|
+
</details>
|
|
@@ -55,32 +55,4 @@
|
|
|
55
55
|
<% end %>
|
|
56
56
|
</section>
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
<summary class="hrl-btn hrl-btn--primary">Upload a document</summary>
|
|
60
|
-
<%= form_with model: @document, url: hr_lite.documents_path, method: :post, local: true, class: "hrl-mt" do |f| %>
|
|
61
|
-
<%= render "hr_lite/shared/form_errors", record: @document %>
|
|
62
|
-
<div class="hrl-field">
|
|
63
|
-
<%= f.label :category, id: "hrl-doc-category", for: nil %>
|
|
64
|
-
<div class="hrl-chiprow" role="radiogroup" aria-labelledby="hrl-doc-category">
|
|
65
|
-
<% (HrLite::Document::SENSITIVE_CATEGORIES.map { |c| [ c.humanize.sub(/\APan\z/, "PAN"), c ] } +
|
|
66
|
-
[ [ "Offer letter", "offer_letter" ], [ "Certificate", "certificate" ], [ "Other", "other" ] ]).each do |label, value| %>
|
|
67
|
-
<label><%= f.radio_button :category, value, required: true %><span><%= label %></span></label>
|
|
68
|
-
<% end %>
|
|
69
|
-
</div>
|
|
70
|
-
</div>
|
|
71
|
-
<div class="hrl-field"><%= f.label :title %><%= f.text_field :title, required: true %></div>
|
|
72
|
-
<div class="hrl-field">
|
|
73
|
-
<%= f.label :reference_number, "Document number (optional)" %>
|
|
74
|
-
<%= f.text_field :reference_number %>
|
|
75
|
-
</div>
|
|
76
|
-
<div class="hrl-field">
|
|
77
|
-
<%= f.label :expires_on, "Expires on (optional — we will remind you)" %>
|
|
78
|
-
<%= f.date_field :expires_on %>
|
|
79
|
-
</div>
|
|
80
|
-
<div class="hrl-field">
|
|
81
|
-
<%= f.label :file, "File (PDF or image, up to 10 MB)" %>
|
|
82
|
-
<%= f.file_field :file, required: true, accept: "application/pdf,image/*" %>
|
|
83
|
-
</div>
|
|
84
|
-
<div class="hrl-form-actions"><%= f.submit "Upload", class: "hrl-btn hrl-btn--primary" %></div>
|
|
85
|
-
<% end %>
|
|
86
|
-
</details>
|
|
58
|
+
<%= render "hr_lite/documents/upload_form", document: @document, url: hr_lite.documents_path, open: @documents.empty? %>
|
data/config/routes.rb
CHANGED
|
@@ -74,7 +74,7 @@ HrLite::Engine.routes.draw do
|
|
|
74
74
|
get "overview", to: "overview#index"
|
|
75
75
|
resources :attendances, only: %i[index show update], param: :user_id do
|
|
76
76
|
collection { get :week }
|
|
77
|
-
member { get :summary; 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 }
|
|
78
78
|
end
|
|
79
79
|
resources :leave_requests, only: %i[index show new create edit update] do
|
|
80
80
|
member { post :approve; post :reject; post :cancel }
|
|
@@ -105,6 +105,7 @@ HrLite::Engine.routes.draw do
|
|
|
105
105
|
member { post :verify; post :reject }
|
|
106
106
|
end
|
|
107
107
|
get "documents/for/:user_id", to: "documents#show", as: :employee_documents
|
|
108
|
+
post "documents/for/:user_id", to: "documents#create"
|
|
108
109
|
resources :tax_declarations, only: %i[index show update] do
|
|
109
110
|
member { post :verify; post :reject; post :accept_all; get "proofs/:proof_id", action: :proof, as: :proof }
|
|
110
111
|
collection { get :overview; get "people/:user_id", action: :person, as: :person }
|
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.52.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kshitiz sinha
|
|
@@ -220,6 +220,7 @@ files:
|
|
|
220
220
|
- app/models/hr_lite/policy_acknowledgement.rb
|
|
221
221
|
- app/models/hr_lite/professional_tax_slab.rb
|
|
222
222
|
- app/models/hr_lite/regularization_request.rb
|
|
223
|
+
- app/models/hr_lite/repeat_rule.rb
|
|
223
224
|
- app/models/hr_lite/resignation.rb
|
|
224
225
|
- app/models/hr_lite/role.rb
|
|
225
226
|
- app/models/hr_lite/role_assignment.rb
|
|
@@ -282,6 +283,7 @@ files:
|
|
|
282
283
|
- app/views/hr_lite/admin/documents/index.html.erb
|
|
283
284
|
- app/views/hr_lite/admin/documents/show.html.erb
|
|
284
285
|
- app/views/hr_lite/admin/employees/_form.html.erb
|
|
286
|
+
- app/views/hr_lite/admin/employees/_shorter_day_fields.html.erb
|
|
285
287
|
- app/views/hr_lite/admin/employees/edit.html.erb
|
|
286
288
|
- app/views/hr_lite/admin/employees/index.html.erb
|
|
287
289
|
- app/views/hr_lite/admin/employees/new.html.erb
|
|
@@ -367,6 +369,7 @@ files:
|
|
|
367
369
|
- app/views/hr_lite/career/show.html.erb
|
|
368
370
|
- app/views/hr_lite/comp_off_requests/index.html.erb
|
|
369
371
|
- app/views/hr_lite/comp_off_requests/new.html.erb
|
|
372
|
+
- app/views/hr_lite/documents/_upload_form.html.erb
|
|
370
373
|
- app/views/hr_lite/documents/index.html.erb
|
|
371
374
|
- app/views/hr_lite/employee_profiles/show.html.erb
|
|
372
375
|
- app/views/hr_lite/event_mailer/event.html.erb
|
|
@@ -499,6 +502,7 @@ files:
|
|
|
499
502
|
- db/migrate/20261015100000_create_hr_lite_attendance_punches.rb
|
|
500
503
|
- db/migrate/20261016090000_add_penalty_leave_type_to_hr_lite_settings.rb
|
|
501
504
|
- db/migrate/20261016091000_create_hr_lite_penalty_removals.rb
|
|
505
|
+
- db/migrate/20261016100000_add_hours_allowance_repeat_to_hr_lite_employee_profiles.rb
|
|
502
506
|
- lib/generators/hr_lite/install/install_generator.rb
|
|
503
507
|
- lib/generators/hr_lite/install/templates/AFTER_INSTALL
|
|
504
508
|
- lib/generators/hr_lite/install/templates/initializer.rb
|