hr_lite 0.64.0 → 0.65.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 +20 -0
- data/app/controllers/hr_lite/hiring_replies_controller.rb +2 -1
- data/app/models/hr_lite/hiring_reply.rb +23 -5
- data/app/models/hr_lite/leave_request.rb +4 -1
- data/app/views/hr_lite/admin/hiring_candidates/show.html.erb +3 -3
- data/app/views/hr_lite/hiring_replies/show.html.erb +20 -21
- data/lib/hr_lite/seeds.rb +21 -1
- data/lib/hr_lite/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e8bac47ad9084b5f1b56da93a9a4658b0657ad90078ad5a51cda3de73d46e93a
|
|
4
|
+
data.tar.gz: e0a6f0dadbda02b024ece4ff00292586077c815022f71cea914a342ccc4a14df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 36a34342b03fe802f84fe9cde43196dc22beeac2fffeb17572d79e8e5cf3b15a4efca9a59616c598f91e7d37efce0fffc8bb7830d35997015982f009477cbeb8
|
|
7
|
+
data.tar.gz: 45a7023d60f36f461dce9fb26482622fb69ef72464b5ff58043284dd785edf2d5ee714c40bec8ffd20f4f5ec07cd905b9f18553c5f1e54eee39bccd31885d385
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.65.0] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
Expense claims have categories to pick from. No migrations.
|
|
13
|
+
|
|
14
|
+
- **`hr_lite:seed` adds 15 travel-company expense categories** (local conveyance, flights and trains, hotel stay, meals, client meetings, site inspections, visa fees, fuel and tolls, airport transfers, phone, courier, printing, trade fairs, tips, Other) when the table is empty. The "What kind of expense" picker was blank on installs nobody had configured. Every category needs a receipt except tips; "Other" uses the required "What it was for" note. An install that already has categories is left alone.
|
|
15
|
+
|
|
16
|
+
## [0.64.2] - 2026-10-24
|
|
17
|
+
|
|
18
|
+
Leadership's leave-decision emails open the leave. No migrations.
|
|
19
|
+
|
|
20
|
+
- **"Leave approved/rejected" emails to leadership linked to the employee's own page** (`/leave_requests/:id`), which 404s for anyone else. Leadership's copy now names the employee and links to `/admin/leave_requests/:id`. The employee's email is unchanged.
|
|
21
|
+
|
|
22
|
+
## [0.64.1] - 2026-10-23
|
|
23
|
+
|
|
24
|
+
Interview slots hand out 30-minute turns. No migrations.
|
|
25
|
+
|
|
26
|
+
- **Up to 4 people per 2-hour slot, a 30-minute turn each.** Turns go in booking order: the 1st at the slot's start, the 2nd 30 minutes later, and so on. A full slot disappears from the form, and a day with no free slots says "Fully booked". A late fifth booking is refused with "Someone just booked that slot".
|
|
27
|
+
- **Everyone sees the turn.** The candidate's thank-you page shows their interview time. HR's candidate page shows it, and "Schedule round N" is prefilled with that start time, so HR never retypes it. Turns are worked out from booking order and never stored.
|
|
28
|
+
- **No email box on the reply page.** Each link already identifies its candidate, and the page still says "This form is for <name>".
|
|
29
|
+
|
|
10
30
|
## [0.64.0] - 2026-10-22
|
|
11
31
|
|
|
12
32
|
Decision emails. One migration: `purpose` on hiring templates.
|
|
@@ -19,7 +19,8 @@ module HrLite
|
|
|
19
19
|
def show; end
|
|
20
20
|
|
|
21
21
|
def create
|
|
22
|
-
|
|
22
|
+
date, slot = params[:pick].to_s.split("|", 2) # one radio per free day + slot
|
|
23
|
+
@reply.submit!(interested: params[:interested].to_s, date: date, slot: slot, joining: params[:joining],
|
|
23
24
|
yes_no: params.permit(*HiringReply::YES_NO.keys).to_h, alternate_phone: params[:alternate_phone].to_s.strip,
|
|
24
25
|
files: params.fetch(:documents, {}).permit(*HiringReply::DOCUMENTS.fetch(@reply.round, {}).keys).to_h,
|
|
25
26
|
kyc: params.permit(:aadhaar_number, :pan_number, :pan_pending, :no_salary_slips, :consent).to_h.symbolize_keys)
|
|
@@ -5,7 +5,8 @@ module HrLite
|
|
|
5
5
|
class HiringReply < ApplicationRecord
|
|
6
6
|
SLOTS = { "10-12" => "10 AM – 12 PM", "12-2" => "12 – 2 PM", "2-4" => "2 – 4 PM", "4-6" => "4 – 6 PM", "6-8" => "6 – 8 PM" }.freeze
|
|
7
7
|
SLOT_START = { "10-12" => "10:00", "12-2" => "12:00", "2-4" => "14:00", "4-6" => "16:00", "6-8" => "18:00" }.freeze
|
|
8
|
-
SLOT_CAPACITY = 4 #
|
|
8
|
+
SLOT_CAPACITY = 4 # people per 2-hour slot per day, then it is gone from the form
|
|
9
|
+
TURN = 30.minutes # each booking gets its own turn, in the order they booked
|
|
9
10
|
JOINING = { "immediately" => "Immediately", "two_weeks" => "Within 2 weeks", "one_month" => "Within a month" }.freeze
|
|
10
11
|
YES_NO = { "stipend_ok" => "Fine with the stipend", "location_ok" => "Fine with the office location",
|
|
11
12
|
"schedule_ok" => "Fine with the schedule", "programme_ok" => "Commits to the programme" }.freeze
|
|
@@ -36,6 +37,13 @@ module HrLite
|
|
|
36
37
|
|
|
37
38
|
def self.slot_full?(date, slot) = responded.where(preferred_date: date, preferred_slot: slot).count >= SLOT_CAPACITY
|
|
38
39
|
|
|
40
|
+
# The slots still free on each offered day, for the reply form.
|
|
41
|
+
def self.open_slots(allow_sunday:)
|
|
42
|
+
days = dates(allow_sunday: allow_sunday)
|
|
43
|
+
booked = responded.where(preferred_date: days).group(:preferred_date, :preferred_slot).count
|
|
44
|
+
days.to_h { |day| [ day, SLOTS.keys.reject { |slot| booked[[ day, slot ]].to_i >= SLOT_CAPACITY } ] }
|
|
45
|
+
end
|
|
46
|
+
|
|
39
47
|
# The next ten interview days. Sunday is off unless the campaign allows it.
|
|
40
48
|
def self.dates(allow_sunday:, from: Date.current) = (1..20).map { |i| from + i }.reject { |d| d.sunday? && !allow_sunday }.first(10)
|
|
41
49
|
|
|
@@ -45,6 +53,18 @@ module HrLite
|
|
|
45
53
|
end
|
|
46
54
|
|
|
47
55
|
# What this round asks for; round 3 skips PAN and slips the candidate says they lack.
|
|
56
|
+
# Their 30-minute turn inside the slot: 1st booking at the start, 2nd 30
|
|
57
|
+
# minutes later, and so on. Worked out from booking order, never stored.
|
|
58
|
+
def turn_starts_at
|
|
59
|
+
return unless responded_at && preferred_slot
|
|
60
|
+
|
|
61
|
+
earlier = self.class.responded.where(preferred_date:, preferred_slot:)
|
|
62
|
+
.where("responded_at < :at OR (responded_at = :at AND id < :id)", at: responded_at, id: id).count
|
|
63
|
+
Time.zone.parse("#{preferred_date} #{SLOT_START.fetch(preferred_slot)}") + (TURN * earlier)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def turn_label = (at = turn_starts_at) && "#{at.strftime('%-l:%M')}–#{(at + TURN).strftime('%-l:%M %p')}"
|
|
67
|
+
|
|
48
68
|
def documents_asked
|
|
49
69
|
asked = DOCUMENTS.fetch(round, {})
|
|
50
70
|
asked = asked.except("pan") if pan_pending
|
|
@@ -61,10 +81,8 @@ module HrLite
|
|
|
61
81
|
|
|
62
82
|
# Saves the reply, moves round 1's candidate to interested / not interested,
|
|
63
83
|
# puts it on their timeline and rings the HR person who emailed them.
|
|
64
|
-
def submit!(
|
|
84
|
+
def submit!(interested: nil, date: nil, slot: nil, joining: nil, yes_no: {}, alternate_phone: nil, files: {}, kyc: {})
|
|
65
85
|
raise ArgumentError, "You have already replied. Thank you." if responded_at
|
|
66
|
-
# The link is personal: the email it was sent to proves the right person opened it.
|
|
67
|
-
raise ArgumentError, "That email does not match this link. Use the link from your own email." unless email.to_s.strip.casecmp?(candidate.email)
|
|
68
86
|
|
|
69
87
|
assign_kyc!(kyc) if round == 3
|
|
70
88
|
|
|
@@ -76,7 +94,7 @@ module HrLite
|
|
|
76
94
|
date = self.class.dates(allow_sunday: candidate.campaign.allow_sunday).find { |day| day.iso8601 == date.to_s }
|
|
77
95
|
raise ArgumentError, "Pick a day for the interview." unless date
|
|
78
96
|
raise ArgumentError, "Pick a time slot." unless SLOTS.key?(slot)
|
|
79
|
-
raise ArgumentError, "
|
|
97
|
+
raise ArgumentError, "Someone just booked that slot. Pick another one." if self.class.slot_full?(date, slot)
|
|
80
98
|
raise ArgumentError, "That phone number does not look right." if alternate_phone.present? && !alternate_phone.to_s.gsub(/[\s-]/, "").match?(/\A\+?\d{7,15}\z/)
|
|
81
99
|
|
|
82
100
|
if round == 1
|
|
@@ -310,7 +310,10 @@ module HrLite
|
|
|
310
310
|
body: decision_note.presence,
|
|
311
311
|
path: "/leave_requests/#{id}",
|
|
312
312
|
bell_to: [ user ],
|
|
313
|
-
email_to: [ user ]
|
|
313
|
+
email_to: [ user ],
|
|
314
|
+
# The employee's path 404s for anyone else; leadership opens the admin record.
|
|
315
|
+
leadership: { title: "#{title} — #{HrLite.display_name(user)}, #{leave_type.name} (#{date_range_label})",
|
|
316
|
+
path: "/admin/leave_requests/#{id}" }
|
|
314
317
|
)
|
|
315
318
|
end
|
|
316
319
|
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
<h2 class="hrl-card__title">Schedule round <%= @interviews.select { |i| %w[scheduled unknown].include?(i.status) }.map(&:round).max.to_i + 1 %></h2>
|
|
35
35
|
<%= form_with url: hr_lite.admin_hiring_candidate_hiring_interviews_path(@candidate), method: :post, local: true do %>
|
|
36
36
|
<% picked = @next_reply %>
|
|
37
|
-
<% if picked %><p class="hrl-small">
|
|
37
|
+
<% if picked %><p class="hrl-small">Their turn: <strong><%= picked.preferred_date.strftime("%A, %-d %B") %>, <%= picked.turn_label %></strong> (in the <%= HrLite::HiringReply::SLOTS[picked.preferred_slot] %> slot). Call to confirm, then send the invite.</p><% end %>
|
|
38
38
|
<div class="hrl-field"><label for="date">Date</label><input type="date" name="date" id="date" required value="<%= picked&.preferred_date&.iso8601 %>"></div>
|
|
39
|
-
<div class="hrl-field"><label for="time">Time</label><input type="time" name="time" id="time" required value="<%=
|
|
39
|
+
<div class="hrl-field"><label for="time">Time</label><input type="time" name="time" id="time" required value="<%= picked&.turn_starts_at&.strftime("%H:%M") %>"></div>
|
|
40
40
|
<div class="hrl-field">
|
|
41
41
|
<label for="minutes">Length</label>
|
|
42
42
|
<select name="minutes" id="minutes"><% HrLite::Admin::HiringInterviewsController::LENGTHS.each do |m| %><option value="<%= m %>"><%= m %> minutes</option><% end %></select>
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
<h2 class="hrl-card__title">Round <%= reply.round %> reply</h2>
|
|
88
88
|
<dl class="hrl-deflist">
|
|
89
89
|
<% if reply.round == 1 %><dt>Interested</dt><dd><%= reply.answers["interested"] == "yes" ? "Yes" : "No" %></dd><% end %>
|
|
90
|
-
<% if reply.preferred_slot %><dt>
|
|
90
|
+
<% if reply.preferred_slot %><dt>Interview time</dt><dd><%= reply.preferred_date.strftime("%A, %-d %B") %>, <%= reply.turn_label %> (<%= HrLite::HiringReply::SLOTS[reply.preferred_slot] %> slot)</dd><% end %>
|
|
91
91
|
<% if reply.answers["joining"] %>
|
|
92
92
|
<dt>Can join</dt><dd><%= HrLite::HiringReply::JOINING[reply.answers["joining"]] %></dd>
|
|
93
93
|
<% HrLite::HiringReply::YES_NO.each do |key, label| %><dt><%= label %></dt><dd><%= reply.answers[key] == "yes" ? "Yes" : "No" %></dd><% end %>
|
|
@@ -9,17 +9,15 @@
|
|
|
9
9
|
<% if @reply.responded_at %>
|
|
10
10
|
<section class="hrl-card">
|
|
11
11
|
<h2 class="hrl-card__title">Thanks, we have your answers</h2>
|
|
12
|
-
|
|
12
|
+
<% if @reply.answers["interested"] == "no" %>
|
|
13
|
+
<p>We have noted that you do not want to go ahead. All the best.</p>
|
|
14
|
+
<% else %>
|
|
15
|
+
<% if (turn = @reply.turn_label) %><p>Your round <%= round %> interview time: <strong><%= @reply.preferred_date.strftime("%A, %-d %B") %>, <%= turn %></strong>.</p><% end %>
|
|
16
|
+
<p>Our HR team will call you to confirm it.</p>
|
|
17
|
+
<% end %>
|
|
13
18
|
</section>
|
|
14
19
|
<% else %>
|
|
15
20
|
<%= form_with url: hr_lite.hiring_reply_path(@reply.token), method: :post, local: true, multipart: true do %>
|
|
16
|
-
<section class="hrl-card">
|
|
17
|
-
<div class="hrl-field">
|
|
18
|
-
<label for="email">Your email (the one this link was sent to)</label>
|
|
19
|
-
<input type="email" name="email" id="email" autocomplete="email" required value="<%= params[:email] %>">
|
|
20
|
-
</div>
|
|
21
|
-
</section>
|
|
22
|
-
|
|
23
21
|
<% if round == 1 %>
|
|
24
22
|
<section class="hrl-card">
|
|
25
23
|
<fieldset class="hrl-field">
|
|
@@ -32,19 +30,20 @@
|
|
|
32
30
|
|
|
33
31
|
<section class="hrl-card">
|
|
34
32
|
<h2 class="hrl-card__title"><%= round == 1 ? "If yes: pick" : "Pick" %> a day and one time slot for round <%= round %></h2>
|
|
35
|
-
<p class="hrl-small hrl-muted">Each slot
|
|
36
|
-
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
<p class="hrl-small hrl-muted">Each 2-hour slot takes up to 4 people. You get your own 30-minute interview time inside it, in the order you book. HR will call you to confirm.</p>
|
|
34
|
+
<% HrLite::HiringReply.open_slots(allow_sunday: campaign.allow_sunday).each do |day, slots| %>
|
|
35
|
+
<fieldset class="hrl-field">
|
|
36
|
+
<legend><%= day.strftime("%A, %-d %B") %></legend>
|
|
37
|
+
<% if slots.empty? %>
|
|
38
|
+
<p class="hrl-small hrl-muted">Fully booked</p>
|
|
39
|
+
<% else %>
|
|
40
|
+
<% slots.each do |slot| %>
|
|
41
|
+
<% value = "#{day.iso8601}|#{slot}" %>
|
|
42
|
+
<label class="hrl-field--check"><input type="radio" name="pick" value="<%= value %>" <%= "checked" if params[:pick] == value %>> <%= HrLite::HiringReply::SLOTS[slot] %></label>
|
|
43
|
+
<% end %>
|
|
44
|
+
<% end %>
|
|
45
|
+
</fieldset>
|
|
46
|
+
<% end %>
|
|
48
47
|
<div class="hrl-field">
|
|
49
48
|
<label for="alternate_phone">Alternate phone number (optional)</label>
|
|
50
49
|
<input type="tel" name="alternate_phone" id="alternate_phone" inputmode="tel" autocomplete="tel" value="<%= params[:alternate_phone] %>">
|
data/lib/hr_lite/seeds.rb
CHANGED
|
@@ -10,8 +10,28 @@ module HrLite
|
|
|
10
10
|
{ code: "CO", name: "Comp off", color: "#8b5cf6", paid: true, annual_quota: 0, accrual: "yearly_upfront", carry_forward_cap: 0, position: 5, comp_off: true }
|
|
11
11
|
].freeze
|
|
12
12
|
|
|
13
|
+
# A travel company's staff claims. "Other" takes anything else; the
|
|
14
|
+
# required "What it was for" field carries the note.
|
|
15
|
+
DEFAULT_EXPENSE_CATEGORIES = [
|
|
16
|
+
"Local conveyance (cab, auto, metro)", "Flights and trains", "Hotel stay",
|
|
17
|
+
"Meals on duty", "Client meeting and entertainment", "Site inspection and FAM trip",
|
|
18
|
+
"Visa, permit and passport fees", "Fuel, tolls and parking", "Airport transfers",
|
|
19
|
+
"Phone and internet", "Courier and documents", "Printing and stationery",
|
|
20
|
+
"Trade fair and events", "Tips and guide payments", "Other"
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
13
23
|
def self.run!
|
|
14
|
-
seed_leave_types! + seed_holidays!
|
|
24
|
+
seed_leave_types! + seed_holidays! + seed_expense_categories!
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Only into an empty table, so renamed or removed categories stay that way.
|
|
28
|
+
def self.seed_expense_categories!
|
|
29
|
+
return [] if ExpenseCategory.exists?
|
|
30
|
+
|
|
31
|
+
DEFAULT_EXPENSE_CATEGORIES.map do |name|
|
|
32
|
+
ExpenseCategory.create!(name: name, receipt_required: name != "Tips and guide payments")
|
|
33
|
+
"expense_category #{name}"
|
|
34
|
+
end
|
|
15
35
|
end
|
|
16
36
|
|
|
17
37
|
# Deliberately NOT auto-run from run!: an operator unticking every
|
data/lib/hr_lite/version.rb
CHANGED