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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 01cbb25a2be4121524133e4ae3cc27c20e703edbe2ad51c41830f89a6223b5d6
4
- data.tar.gz: ba0ed1ceeda7d1c25eec0a9607e773b260cd255eadee1f68b4d35667ab0de9d1
3
+ metadata.gz: 6668e3a20cc3b84ebe77b9ae78ff2c20a12c37af21193d88b5f723a1383a7dca
4
+ data.tar.gz: ec7757fc4f4c0220dd21f05037b4054221a76ae445306c32911a2e71dcdd841d
5
5
  SHA512:
6
- metadata.gz: a50669d8acec885fcaa6304e071785a34432c1bd8314edf57faac5f5436651b186e17c226e3413f50bb872f38fc52859dd61c0f248bd2ab09f03215c9a9d962f
7
- data.tar.gz: 92b42d0e6301c22de05a6eed335f84065ad81eb5fd8ae3d3887dae0f38c66b485bcec3f9b13b7ea957ebf3dda4b16a38d891d69e351356bd91b50bb69d9a1877
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
- @employee = HrLite.user_klass.find(params[:user_id])
21
- hr_require_reach!("document.view", @employee)
22
- @documents = Document.for_user(@employee).recent_first
23
- @missing = Document::SENSITIVE_CATEGORIES - @documents.map(&:category)
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 Settings.
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 allowance.
45
- def self.day_minutes_for(user_id) = Setting.instance.day_minutes - where(user_id: user_id).pick(:hours_allowance_minutes).to_i
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
- <div class="hrl-field">
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.where(user_id: hr_current_user.id).pick(:hours_allowance_minutes) %>
27
- <span class="hrl-badge hrl-badge--muted">Your day <%= hrl_duration(HrLite::EmployeeProfile.day_minutes_for(hr_current_user.id) * 60) %></span>
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
- <details class="hrl-card" <%= "open" if @documents.empty? || @document.errors.any? %>>
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 }
@@ -0,0 +1,5 @@
1
+ class AddHoursAllowanceRepeatToHrLiteEmployeeProfiles < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :hr_lite_employee_profiles, :hours_allowance_repeat, :string
4
+ end
5
+ end
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.50.0"
2
+ VERSION = "0.52.0"
3
3
  end
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.50.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