hr_lite 0.64.0 → 0.64.1

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: 86b218a8a46bb4b5c3527df1fb637aee4b7617e8ef8abfd4e018274bc08e05af
4
- data.tar.gz: 3e645c3511f89479289ac9169317183232d54c7443846208062d9b764bf692f0
3
+ metadata.gz: 7771723b1709c1d61c4b26737d76980d3eb00d0896f68550380e8c8605e35f77
4
+ data.tar.gz: 072a2fd9cc465306163c5bf3005eb8d9d69609f5122f05cd4c93839fc5b15b5f
5
5
  SHA512:
6
- metadata.gz: 8ac6f7b8261f2dbaa74acb697effd3deb137d6f3d84647df9260c602ea457d7cb7067b631bdcbef4c09d56555f5688884bcac7607b48ae52002b0946291e4597
7
- data.tar.gz: dfd0bb1099a90b72051f91569303e321d1d3a4cd216baf3cd9ba11f4dae720e5738be651fb584863b2fbe080702f0b8b517c70f49c9223f3ae6563cf72c0b6e6
6
+ metadata.gz: ac2806cb7d5c5d503b8f9136cc893166f3f1d6e7b46e6d49c25a1be7b0aba8c4b0c66eeaffd22229d96efbe89f14d5c3b510019da338697bda771460854b939b
7
+ data.tar.gz: d39889da25d341b8eb2d5353ade577229971f2c6eee5d8028f722af6115ec0bb3b76404761b683dc6c25dd5c04d67407ddd79fe5d905e076492d2aa010a84d06
data/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.64.1] - 2026-10-23
11
+
12
+ Interview slots hand out 30-minute turns. No migrations.
13
+
14
+ - **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".
15
+ - **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.
16
+ - **No email box on the reply page.** Each link already identifies its candidate, and the page still says "This form is for <name>".
17
+
10
18
  ## [0.64.0] - 2026-10-22
11
19
 
12
20
  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
- @reply.submit!(email: params[:email], interested: params[:interested].to_s, date: params[:date], slot: params[:slot], joining: params[:joining],
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 # candidates per slot per day; HR calls each to confirm
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!(email: nil, interested: nil, date: nil, slot: nil, joining: nil, yes_no: {}, alternate_phone: nil, files: {}, kyc: {})
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, "That slot just filled up. Pick another one." if self.class.slot_full?(date, slot)
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
@@ -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">They picked <strong><%= picked.preferred_date.strftime("%A, %-d %B") %>, <%= HrLite::HiringReply::SLOTS[picked.preferred_slot] %></strong>. Call to confirm, then send the invite.</p><% end %>
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="<%= HrLite::HiringReply::SLOT_START[picked&.preferred_slot] %>"></div>
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>Slot</dt><dd><%= reply.preferred_date.strftime("%A, %-d %B") %>, <%= HrLite::HiringReply::SLOTS[reply.preferred_slot] %></dd><% end %>
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
- <p><%= @reply.answers["interested"] == "no" ? "We have noted that you do not want to go ahead. All the best." : "Our HR team will call you to confirm your round #{round} interview slot." %></p>
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 is 2 hours. HR will call you to confirm.</p>
36
- <fieldset class="hrl-field">
37
- <legend>Day</legend>
38
- <% HrLite::HiringReply.dates(allow_sunday: campaign.allow_sunday).each do |day| %>
39
- <label class="hrl-field--check"><input type="radio" name="date" value="<%= day.iso8601 %>" <%= "checked" if params[:date] == day.iso8601 %>> <%= day.strftime("%A, %-d %B") %></label>
40
- <% end %>
41
- </fieldset>
42
- <fieldset class="hrl-field">
43
- <legend>Time slot</legend>
44
- <% HrLite::HiringReply::SLOTS.each do |key, label| %>
45
- <label class="hrl-field--check"><input type="radio" name="slot" value="<%= key %>" <%= "checked" if params[:slot] == key %>> <%= label %></label>
46
- <% end %>
47
- </fieldset>
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] %>">
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.64.0"
2
+ VERSION = "0.64.1"
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.64.0
4
+ version: 0.64.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - kshitiz sinha