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 +4 -4
- data/CHANGELOG.md +21 -0
- data/app/assets/stylesheets/hr_lite/hr_lite.css +4 -0
- data/app/controllers/hr_lite/admin/hiring_candidates_controller.rb +16 -1
- data/app/controllers/hr_lite/admin/hiring_sends_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/hiring_templates_controller.rb +7 -4
- data/app/controllers/hr_lite/hiring_replies_controller.rb +1 -1
- data/app/helpers/hr_lite/application_helper.rb +1 -0
- data/app/models/hr_lite/attendance_penalty.rb +13 -0
- data/app/models/hr_lite/hiring_reply.rb +3 -1
- data/app/models/hr_lite/hiring_template.rb +8 -0
- data/app/services/hr_lite/hiring_merge.rb +5 -2
- data/app/views/hr_lite/admin/hiring_candidates/show.html.erb +2 -2
- data/app/views/hr_lite/admin/hiring_templates/_form.html.erb +4 -0
- data/app/views/hr_lite/admin/hiring_templates/edit.html.erb +4 -1
- data/app/views/hr_lite/admin/hiring_templates/index.html.erb +22 -1
- data/app/views/hr_lite/admin/hiring_templates/new.html.erb +4 -1
- data/app/views/hr_lite/attendance/_penalties.html.erb +12 -6
- data/app/views/hr_lite/hiring_replies/show.html.erb +8 -1
- data/db/migrate/20261022120000_add_purpose_to_hr_lite_hiring_templates.rb +8 -0
- data/lib/hr_lite/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 86b218a8a46bb4b5c3527df1fb637aee4b7617e8ef8abfd4e018274bc08e05af
|
|
4
|
+
data.tar.gz: 3e645c3511f89479289ac9169317183232d54c7443846208062d9b764bf692f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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"
|
|
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.
|
|
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"
|
|
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
|
|
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
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
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
|
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.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
|