hr_lite 0.62.0 → 0.64.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: 7f779cdbd416cb664617888bbb71b2c6866742b52e3ad5525ac2c5819a9b9908
4
- data.tar.gz: 346563b5b820eabc5aec4d83076cb1a3dd37f6309c159ad15c7a3f70698032a3
3
+ metadata.gz: 86b218a8a46bb4b5c3527df1fb637aee4b7617e8ef8abfd4e018274bc08e05af
4
+ data.tar.gz: 3e645c3511f89479289ac9169317183232d54c7443846208062d9b764bf692f0
5
5
  SHA512:
6
- metadata.gz: 682e1b50a140c01d357a77666d721629e713dc8c1f97465ea8943c92b8d0cee5a5e47933c98f9451d5e80019bdc548879a27f1c72002279a19ab051500873ce6
7
- data.tar.gz: 57fa8aa33593a9f4ac0d88abf82dcc47dd32f666872924038ffb1de4754cb981312fec76e23fcd9706bc31b2aa75a3dc40a42bd25b8d4c947d26705b2f356f2c
6
+ metadata.gz: 8ac6f7b8261f2dbaa74acb697effd3deb137d6f3d84647df9260c602ea457d7cb7067b631bdcbef4c09d56555f5688884bcac7607b48ae52002b0946291e4597
7
+ data.tar.gz: dfd0bb1099a90b72051f91569303e321d1d3a4cd216baf3cd9ba11f4dae720e5738be651fb584863b2fbe080702f0b8b517c70f49c9223f3ae6563cf72c0b6e6
data/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.64.0] - 2026-10-22
11
+
12
+ Decision emails. One migration: `purpose` on hiring templates.
13
+
14
+ - **Selected, Rejected and On hold each send a saved email.** The first time a button is used with no email set, HR is taken to write it, with Write with AI on hand. Then they return to the candidate and click again. After that, every click moves the stage and sends that email through the HR person's own Gmail, once per candidate. Decisions are refused while the mailbox is not connected.
15
+ - **Decision emails can be edited, never deleted.** There is one per decision, and the purpose never changes. The templates page lists them first, flagging any that are not set.
16
+
17
+ ## [0.63.1] - 2026-10-22
18
+
19
+ A reply link can only be answered by the candidate it belongs to. No migrations.
20
+
21
+ - **Previews never show a real reply link.** The template preview and the send-confirm check render "[Name's round N link]" instead, so HR can no longer open a previewed candidate's link and answer as them. A real link is made only when an email is actually sent.
22
+ - **The reply page names its owner and checks their email.** It says "This form is for <full name>" and asks for the email the link was sent to. A different email is refused and nothing is saved.
23
+
24
+ ## [0.63.0] - 2026-10-22
25
+
26
+ Every penalty row now says why it exists. No migrations.
27
+
28
+ - **Penalty cause, behind an "i".** Tap a row in "Penalties in <month>" (or its "i") to see the day and punch that caused it and what it adds to the day it sits on, e.g. "You checked in at 19:39 on Mon 19 Oct, after 11:00, so +1h goes on your next 2 working days. So Thu 22 Oct needs 60 min more before checkout." A carried penalty (late after 11:00, or left early) used to show only the day it landed on, so it read as a penalty for a day the person was on time.
29
+ - **How to avoid or clear them.** The Penalty hours card adds: check in by your slot and stay until the required checkout; if a punch was wrong, fix that day and ask HR to remove the penalties it caused.
30
+
10
31
  ## [0.62.0] - 2026-10-22
11
32
 
12
33
  Hiring replies per round, an AI draft button and the sender's signature. Two migrations: a new `hr_lite_hiring_replies` table, and `allow_sunday` on campaigns.
@@ -294,6 +294,10 @@ details > form > button[type=submit], details > form > .hrl-btn { margin-top: .2
294
294
  .hrl-btn[disabled] { opacity: .55; cursor: default; }
295
295
  summary.hrl-btn { list-style: none; }
296
296
  summary.hrl-btn::-webkit-details-marker { display: none; } /* Safari before 18.4 draws it whatever the display */
297
+ /* A line with an "i": tap anywhere on it for why; the reason drops below. */
298
+ .hrl-why > summary { display: flex; align-items: center; justify-content: space-between; gap: .5rem; min-height: 44px; list-style: none; cursor: pointer; }
299
+ .hrl-why > summary::-webkit-details-marker { display: none; }
300
+ .hrl-why > summary > .hrl-icon { color: var(--hrl-accent); }
297
301
 
298
302
  /* Badges & chips */
299
303
  .hrl-badge {
@@ -38,10 +38,25 @@ module HrLite
38
38
  render :show
39
39
  end
40
40
 
41
+ # Selected / Rejected / On hold also send that decision's saved email. With
42
+ # none saved yet, HR writes it first; the decision waits until it exists.
41
43
  def stage
42
44
  to = params[:to].presence_in(MANUAL) or raise ArgumentError, "That is not a decision HR makes here."
45
+ raise ArgumentError, "#{@candidate.stage_label} cannot move to #{to.humanize}." unless HiringCandidate::MOVES.fetch(@candidate.stage).include?(to)
46
+
47
+ template = HiringTemplate.for_decision(to)
48
+ if HiringTemplate::PURPOSES.key?(to) && !template
49
+ return redirect_to hr_lite.new_admin_hiring_template_path(purpose: to, candidate_id: @candidate.id),
50
+ alert: "Write the #{HiringTemplate::PURPOSES[to]} email first. Every #{HiringTemplate::PURPOSES[to]} click sends it."
51
+ end
52
+ raise ArgumentError, "Connect your Google account for hiring before sending decision emails." if template && !HrLite.config.hiring&.sender&.dig(:ready)
53
+
43
54
  @candidate.move_to!(to, reason: params[:reason], note: params[:note])
44
- redirect_to hr_lite.admin_hiring_candidate_path(@candidate), notice: "Moved to #{@candidate.stage_label}."
55
+ if template && (message = HiringMessage.create_or_find_by!(candidate: @candidate, template: template) { |m| m.batch = SecureRandom.uuid; m.sent_by = hr_current_user }).previously_new_record?
56
+ HiringSendJob.perform_later(message.id)
57
+ end
58
+ redirect_to hr_lite.admin_hiring_candidate_path(@candidate),
59
+ notice: "Moved to #{@candidate.stage_label}.#{" The #{template.name} email is on its way." if template}"
45
60
  rescue ArgumentError => e
46
61
  redirect_to hr_lite.admin_hiring_candidate_path(@candidate), alert: e.message
47
62
  end
@@ -52,7 +52,7 @@ module HrLite
52
52
  already = HiringMessage.where(template: @template, candidate_id: scope.select(:id)).pluck(:candidate_id).to_set
53
53
  @recipients, @left_out = [], []
54
54
  scope.includes(:campaign, :interviews).each do |candidate|
55
- missing = HiringMerge.new(candidate, sender_name: @sender&.dig(:name)).render(@template).missing
55
+ missing = HiringMerge.new(candidate, sender_name: @sender&.dig(:name), preview: true).render(@template).missing
56
56
  reason = already.include?(candidate.id) ? "already sent this template" : (missing.join(", ") if missing.any?)
57
57
  reason ? @left_out << [ candidate, reason ] : @recipients << candidate
58
58
  end
@@ -10,13 +10,16 @@ module HrLite
10
10
  with: -> { redirect_to hr_lite.admin_hiring_templates_path, alert: "Twenty AI drafts an hour is the limit. Try again later." }
11
11
 
12
12
  def index = @templates = HiringTemplate.alphabetical
13
- def new = @template = HiringTemplate.new
13
+ def new = @template = HiringTemplate.new(purpose: params[:purpose].presence_in(HiringTemplate::PURPOSES.keys))
14
14
 
15
15
  def create
16
- @template = HiringTemplate.new(template_params)
16
+ @template = HiringTemplate.new(template_params.merge(purpose: params.dig(:hiring_template, :purpose).presence))
17
17
  return draft if params[:ai].present?
18
18
  return render(:new, status: :unprocessable_entity) unless @template.save
19
19
 
20
+ candidate = HiringCandidate.find_by(id: params[:candidate_id]) if @template.purpose
21
+ return redirect_to(hr_lite.admin_hiring_candidate_path(candidate), notice: "Saved. Click #{HiringTemplate::PURPOSES[@template.purpose]} again to send it.") if candidate
22
+
20
23
  redirect_to hr_lite.edit_admin_hiring_template_path(@template), notice: "Saved."
21
24
  end
22
25
 
@@ -58,10 +61,10 @@ module HrLite
58
61
  return true unless @preview_for
59
62
 
60
63
  sender_name = HrLite.config.hiring&.sender&.dig(:name)
61
- @preview = HiringMerge.new(@preview_for, sender_name:).render(@template)
64
+ @preview = HiringMerge.new(@preview_for, sender_name:, preview: true).render(@template)
62
65
  # Readiness across the previewed candidate's campaign: who a send would reach.
63
66
  @campaign_size = @preview_for.campaign.candidates.count
64
- @ready = @preview_for.campaign.candidates.includes(:campaign, :interviews).count { |c| HiringMerge.new(c, sender_name:).render(@template).missing.empty? }
67
+ @ready = @preview_for.campaign.candidates.includes(:campaign, :interviews).count { |c| HiringMerge.new(c, sender_name:, preview: true).render(@template).missing.empty? }
65
68
  true
66
69
  end
67
70
 
@@ -19,7 +19,7 @@ module HrLite
19
19
  def show; end
20
20
 
21
21
  def create
22
- @reply.submit!(interested: params[:interested].to_s, date: params[:date], slot: params[:slot], joining: params[:joining],
22
+ @reply.submit!(email: params[:email], interested: params[:interested].to_s, date: params[:date], slot: params[:slot], joining: params[:joining],
23
23
  yes_no: params.permit(*HiringReply::YES_NO.keys).to_h, alternate_phone: params[:alternate_phone].to_s.strip,
24
24
  files: params.fetch(:documents, {}).permit(*HiringReply::DOCUMENTS.fetch(@reply.round, {}).keys).to_h,
25
25
  kyc: params.permit(:aadhaar_number, :pan_number, :pan_pending, :no_salary_slips, :consent).to_h.symbolize_keys)
@@ -198,6 +198,7 @@ module HrLite
198
198
  "People" => '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8"/>',
199
199
  "Me" => '<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>',
200
200
  "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"/>',
201
+ "Info" => '<circle cx="12" cy="12" r="9"/><path d="M12 11v6M12 7.5h.01"/>',
201
202
  "Hiring" => '<rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>',
202
203
  "Admin" => '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
203
204
  "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"/>',
@@ -36,5 +36,18 @@ module HrLite
36
36
  def self.days_for(minutes) = (minutes / 300) * 0.5
37
37
 
38
38
  def label = REASONS.fetch(reason)
39
+
40
+ # Why this row exists, naming the day and punch that caused it.
41
+ def cause
42
+ r = attendance_record
43
+ t = ->(at) { at.nil? ? "—" : at.in_time_zone(HrLite.config.time_zone).then { |l| l.strftime(l.to_date == r.date ? "%H:%M" : "%H:%M on %a %-d %b") } }
44
+ on = r.date.strftime("%a %-d %b")
45
+ why = case reason
46
+ when "late" then "You checked in at #{t[r.check_in_at]} on #{on}, after your slot."
47
+ when "late_carry" then "You checked in at #{t[r.check_in_at]} on #{on}, after 11:00, so +1h goes on your next 2 working days."
48
+ else "You checked out at #{t[r.check_out_at]} on #{on}, before that day's required checkout of #{t[r.required_check_out_at]}."
49
+ end
50
+ "#{why} So #{date.strftime('%a %-d %b')} needs #{minutes} min more before checkout."
51
+ end
39
52
  end
40
53
  end
@@ -61,8 +61,10 @@ module HrLite
61
61
 
62
62
  # Saves the reply, moves round 1's candidate to interested / not interested,
63
63
  # puts it on their timeline and rings the HR person who emailed them.
64
- def submit!(interested: nil, date: nil, slot: nil, joining: nil, yes_no: {}, alternate_phone: nil, files: {}, kyc: {})
64
+ def submit!(email: nil, interested: nil, date: nil, slot: nil, joining: nil, yes_no: {}, alternate_phone: nil, files: {}, kyc: {})
65
65
  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)
66
68
 
67
69
  assign_kyc!(kyc) if round == 3
68
70
 
@@ -4,7 +4,15 @@ module HrLite
4
4
  class HiringTemplate < ApplicationRecord
5
5
  has_many :messages, class_name: "HrLite::HiringMessage", foreign_key: :template_id, dependent: :restrict_with_error
6
6
 
7
+ # The emails the Selected / Rejected / On hold buttons send. One each; edited,
8
+ # never deleted (there is no destroy), and the purpose never changes.
9
+ PURPOSES = { "selected" => "Selected", "rejected" => "Rejected", "on_hold" => "On hold" }.freeze
10
+
7
11
  validates :name, :subject, :body, presence: true
12
+ validates :purpose, inclusion: { in: PURPOSES.keys }, uniqueness: true, allow_nil: true
13
+ attr_readonly :purpose
14
+
15
+ def self.for_decision(stage) = find_by(purpose: stage)
8
16
 
9
17
  scope :alphabetical, -> { order(:name) }
10
18
  end
@@ -14,8 +14,11 @@ module HrLite
14
14
 
15
15
  def self.tokens(text) = text.to_s.scan(TOKEN).flatten.uniq
16
16
 
17
- def initialize(candidate, sender_name:)
17
+ # preview: true shows a label for each reply link instead of the real one, so
18
+ # nobody can open a candidate's link from a preview and answer as them.
19
+ def initialize(candidate, sender_name:, preview: false)
18
20
  @extra = candidate.extra.to_h.transform_keys(&:to_s)
21
+ @preview = preview
19
22
  @values = @extra.merge(reserved(candidate, sender_name))
20
23
  end
21
24
 
@@ -48,7 +51,7 @@ module HrLite
48
51
  "interview_date" => at&.strftime("%-d %B %Y"), "interview_time" => at&.strftime("%-l:%M %p"),
49
52
  "meet_link" => interview&.meet_link, "sender_name" => sender_name,
50
53
  # Lazy: a round's link (and its row) is made only when a template names it.
51
- **(1..3).to_h { |r| [ "round#{r}_link", -> { HrLite.public_url("/apply/#{candidate.reply_for(r).token}") } ] } }
54
+ **(1..3).to_h { |r| [ "round#{r}_link", -> { @preview ? "[#{candidate.name}'s round #{r} link]" : HrLite.public_url("/apply/#{candidate.reply_for(r).token}") } ] } }
52
55
  end
53
56
  end
54
57
  end
@@ -58,7 +58,7 @@
58
58
  <div class="hrl-form-actions hrl-hiring-decide">
59
59
  <% (manual - [ "rejected" ]).each do |to| %>
60
60
  <%= button_to to.humanize, hr_lite.stage_admin_hiring_candidate_path(@candidate), method: :patch, params: { to: to }, class: "hrl-btn",
61
- form: { data: { turbo_confirm: ("Mark #{@candidate.name} selected? This is final." if to == "selected") } } %>
61
+ form: { data: { turbo_confirm: ("Mark #{@candidate.name} #{to.humanize.downcase}?#{" This is final." if to == "selected"}#{" They get the #{to.humanize} email." if HrLite::HiringTemplate::PURPOSES.key?(to)}" if HrLite::HiringTemplate::PURPOSES.key?(to)) } } %>
62
62
  <% end %>
63
63
  </div>
64
64
  <% if manual.include?("rejected") %>
@@ -74,7 +74,7 @@
74
74
  </select>
75
75
  </div>
76
76
  <div class="hrl-field"><label for="note">Note (needed for “Other”)</label><textarea name="note" id="note" rows="2"></textarea></div>
77
- <div class="hrl-form-actions"><button class="hrl-btn hrl-btn--danger" type="submit" data-turbo-confirm="Reject <%= @candidate.name %>? This is final.">Confirm rejection</button></div>
77
+ <div class="hrl-form-actions"><button class="hrl-btn hrl-btn--danger" type="submit" data-turbo-confirm="Reject <%= @candidate.name %>? This is final. They get the Rejected email.">Confirm rejection</button></div>
78
78
  <% end %>
79
79
  </details>
80
80
  <% end %>
@@ -1,5 +1,9 @@
1
1
  <%= form_with model: template, url: (template.persisted? ? hr_lite.admin_hiring_template_path(template) : hr_lite.admin_hiring_templates_path), local: true do |f| %>
2
2
  <%= render "hr_lite/shared/form_errors", record: template %>
3
+ <% if template.new_record? && template.purpose %>
4
+ <input type="hidden" name="hiring_template[purpose]" value="<%= template.purpose %>">
5
+ <% if params[:candidate_id].present? %><input type="hidden" name="candidate_id" value="<%= params[:candidate_id] %>"><% end %>
6
+ <% end %>
3
7
  <div class="hrl-field"><%= f.label :name, "Template name" %><%= f.text_field :name, required: true %></div>
4
8
  <div class="hrl-field"><%= f.label :subject %><%= f.text_field :subject, required: true %></div>
5
9
  <div class="hrl-field"><%= f.label :body, "Message" %><%= f.text_area :body, rows: 16, required: true %></div>
@@ -1,5 +1,8 @@
1
1
  <% content_for(:page_title) { @template.name } %>
2
- <div class="hrl-page__head"><h1 class="hrl-page__title"><%= @template.name %></h1></div>
2
+ <div class="hrl-page__head">
3
+ <h1 class="hrl-page__title"><%= @template.name %></h1>
4
+ <% if @template.purpose %><p class="hrl-page__sub"><span class="hrl-badge">Decision email: <%= HrLite::HiringTemplate::PURPOSES[@template.purpose] %></span> Sent when you click <%= HrLite::HiringTemplate::PURPOSES[@template.purpose] %>. It can be edited, not deleted.</p><% end %>
5
+ </div>
3
6
 
4
7
  <% if @preview %>
5
8
  <div class="hrl-flash <%= @ready == @campaign_size ? "hrl-flash--ok" : "hrl-flash--warn" %>">
@@ -3,9 +3,30 @@
3
3
  <h1 class="hrl-page__title">Hiring email templates</h1>
4
4
  <div class="hrl-page__actions"><a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_admin_hiring_template_path %>">New template</a></div>
5
5
  </div>
6
+
6
7
  <section class="hrl-card">
8
+ <h2 class="hrl-card__title">Decision emails</h2>
9
+ <p class="hrl-small hrl-muted">Sent when you click Selected, Rejected or On hold on a candidate. You can edit them; they cannot be deleted.</p>
7
10
  <ul class="hrl-feed">
8
- <% @templates.each do |template| %>
11
+ <% decisions = @templates.select(&:purpose).index_by(&:purpose) %>
12
+ <% HrLite::HiringTemplate::PURPOSES.each do |purpose, label| %>
13
+ <li class="hrl-feed__item"><div class="hrl-feed__body">
14
+ <% if (template = decisions[purpose]) %>
15
+ <a class="hrl-card__link" href="<%= hr_lite.edit_admin_hiring_template_path(template) %>"><%= label %>: <%= template.name %></a>
16
+ <span class="hrl-feed__meta"><%= template.subject %></span>
17
+ <% else %>
18
+ <strong><%= label %></strong> <span class="hrl-badge hrl-badge--warn">Not set</span>
19
+ <a class="hrl-btn" href="<%= hr_lite.new_admin_hiring_template_path(purpose: purpose) %>">Write the <%= label %> email</a>
20
+ <% end %>
21
+ </div></li>
22
+ <% end %>
23
+ </ul>
24
+ </section>
25
+
26
+ <section class="hrl-card">
27
+ <h2 class="hrl-card__title">Other emails</h2>
28
+ <ul class="hrl-feed">
29
+ <% @templates.reject(&:purpose).each do |template| %>
9
30
  <li class="hrl-feed__item"><div class="hrl-feed__body">
10
31
  <a class="hrl-card__link" href="<%= hr_lite.edit_admin_hiring_template_path(template) %>"><%= template.name %></a>
11
32
  <span class="hrl-feed__meta"><%= template.subject %></span>
@@ -1,3 +1,6 @@
1
1
  <% content_for(:page_title) { "New template" } %>
2
- <div class="hrl-page__head"><h1 class="hrl-page__title">New hiring email</h1></div>
2
+ <div class="hrl-page__head">
3
+ <h1 class="hrl-page__title"><%= @template.purpose ? "The #{HrLite::HiringTemplate::PURPOSES[@template.purpose]} email" : "New hiring email" %></h1>
4
+ <% if @template.purpose %><p class="hrl-page__sub">Sent every time someone clicks <%= HrLite::HiringTemplate::PURPOSES[@template.purpose] %> on a candidate. Paste what you would write, then use Write with AI to tidy it and add the fields.</p><% end %>
5
+ </div>
3
6
  <section class="hrl-card"><%= render "form", template: @template %></section>
@@ -4,7 +4,7 @@
4
4
  <section class="hrl-card" id="penalties">
5
5
  <h2 class="hrl-card__title">Penalty hours</h2>
6
6
  <p><strong><%= hrl_duration(held * 60) %></strong> held · <%= pluralize(hrl_days(HrLite::AttendancePenalty.days_for(held)), "day") %><%= " of #{leave_type.name}" if leave_type %></p>
7
- <p class="hrl-hint">Penalties add up until HR removes them.<% if leave_type %> Every 5 hours holds half a day of leave, which can't be applied for.<% end %></p>
7
+ <p class="hrl-hint">Penalties add up until HR removes them.<% if leave_type %> Every 5 hours holds half a day of leave, which can't be applied for.<% end %> To avoid more, check in by your slot and stay until the required checkout. If a punch was wrong, fix that day and ask HR to remove the penalties it caused.</p>
8
8
  <% HrLite::PenaltyRemoval.where(user_id: user.id).includes(:removed_by).order(created_at: :desc).each do |removal| %>
9
9
  <div class="hrl-feed__item hrl-small"><div class="hrl-feed__body">Removed <%= hrl_duration(removal.minutes * 60) %> on <%= removal.created_at.strftime("%-d %b %Y") %> by <%= hr_display_name(removal.removed_by) %>: <%= removal.reason %></div></div>
10
10
  <% end %>
@@ -16,7 +16,7 @@
16
16
  <% end %>
17
17
  <% end %>
18
18
  </section>
19
- <% penalties = HrLite::AttendancePenalty.where(user_id: user.id, date: month.all_month).order(:date, :id) %>
19
+ <% penalties = HrLite::AttendancePenalty.where(user_id: user.id, date: month.all_month).includes(:attendance_record).order(:date, :id) %>
20
20
  <section class="hrl-card">
21
21
  <h2 class="hrl-card__title">Penalties in <%= month.strftime("%B") %></h2>
22
22
  <% if penalties.empty? %>
@@ -25,13 +25,19 @@
25
25
  <% penalties.each do |penalty| %>
26
26
  <div class="hrl-feed__item hrl-small">
27
27
  <div class="hrl-feed__body">
28
- <strong><%= penalty.date.strftime("%a %-d %b") %></strong> ·
29
- <span class="<%= class_names("hrl-badge", penalty.removed_at ? "hrl-badge--muted" : "hrl-badge--warn") %>">+<%= penalty.minutes %> min</span>
30
- <%= penalty.label %>
28
+ <details class="hrl-why">
29
+ <summary>
30
+ <span><strong><%= penalty.date.strftime("%a %-d %b") %></strong> ·
31
+ <span class="<%= class_names("hrl-badge", penalty.removed_at ? "hrl-badge--muted" : "hrl-badge--warn") %>">+<%= penalty.minutes %> min</span>
32
+ <%= penalty.label %></span>
33
+ <%= hrl_icon("Info") %><span class="hrl-sr-only">Why?</span>
34
+ </summary>
35
+ <div class="hrl-muted"><%= penalty.cause %></div>
36
+ </details>
31
37
  <% if penalty.removed_at %>
32
38
  <div class="hrl-muted">Removed by HR: <%= penalty.removal_reason %></div>
33
39
  <% elsif local_assigns[:removable] %>
34
- <%= form_with url: hr_lite.remove_penalty_admin_attendance_path(user.id, penalty_id: penalty.id), method: :post, local: true, class: "hrl-row" do %>
40
+ <%= form_with url: hr_lite.remove_penalty_admin_attendance_path(user.id, penalty_id: penalty.id), method: :post, local: true, class: "hrl-row hrl-mt" do %>
35
41
  <label class="hrl-sr-only" for="hrl-remove-<%= penalty.id %>">Reason to remove</label>
36
42
  <input type="text" class="hrl-input" name="removal_reason" id="hrl-remove-<%= penalty.id %>" required placeholder="Reason to remove">
37
43
  <button type="submit" class="hrl-btn">Remove</button>
@@ -3,7 +3,7 @@
3
3
  <% content_for(:page_title) { campaign.position } %>
4
4
  <div class="hrl-page__head">
5
5
  <h1 class="hrl-page__title"><%= campaign.position %></h1>
6
- <p class="hrl-page__sub">Hi <%= @candidate.first_name %>, <%= round == 1 ? "thank you for your interest." : "congratulations on reaching round #{round}." %></p>
6
+ <p class="hrl-page__sub">This form is for <strong><%= @candidate.name %></strong>. Hi <%= @candidate.first_name %>, <%= round == 1 ? "thank you for your interest." : "congratulations on reaching round #{round}." %></p>
7
7
  </div>
8
8
 
9
9
  <% if @reply.responded_at %>
@@ -13,6 +13,13 @@
13
13
  </section>
14
14
  <% else %>
15
15
  <%= 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
+
16
23
  <% if round == 1 %>
17
24
  <section class="hrl-card">
18
25
  <fieldset class="hrl-field">
@@ -0,0 +1,8 @@
1
+ class AddPurposeToHrLiteHiringTemplates < ActiveRecord::Migration[8.1]
2
+ # The email a decision button sends (selected / rejected / on_hold). One per
3
+ # purpose; nil for ordinary templates.
4
+ def change
5
+ add_column :hr_lite_hiring_templates, :purpose, :string
6
+ add_index :hr_lite_hiring_templates, :purpose, unique: true, where: "purpose IS NOT NULL"
7
+ end
8
+ end
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.62.0"
2
+ VERSION = "0.64.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.62.0
4
+ version: 0.64.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kshitiz sinha
@@ -547,6 +547,7 @@ files:
547
547
  - db/migrate/20261021100500_seed_hr_lite_hiring.rb
548
548
  - db/migrate/20261022090000_create_hr_lite_hiring_replies.rb
549
549
  - db/migrate/20261022090100_add_allow_sunday_to_hr_lite_hiring_campaigns.rb
550
+ - db/migrate/20261022120000_add_purpose_to_hr_lite_hiring_templates.rb
550
551
  - lib/generators/hr_lite/install/install_generator.rb
551
552
  - lib/generators/hr_lite/install/templates/AFTER_INSTALL
552
553
  - lib/generators/hr_lite/install/templates/initializer.rb