hr_lite 0.60.0 → 0.61.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/README.md +14 -0
  4. data/app/assets/stylesheets/hr_lite/hr_lite.css +20 -0
  5. data/app/controllers/hr_lite/admin/hiring_campaigns_controller.rb +88 -0
  6. data/app/controllers/hr_lite/admin/hiring_candidates_controller.rb +27 -0
  7. data/app/controllers/hr_lite/admin/hiring_interviews_controller.rb +77 -0
  8. data/app/controllers/hr_lite/admin/hiring_sends_controller.rb +62 -0
  9. data/app/controllers/hr_lite/admin/hiring_templates_controller.rb +45 -0
  10. data/app/helpers/hr_lite/application_helper.rb +38 -0
  11. data/app/jobs/hr_lite/hiring_send_job.rb +42 -0
  12. data/app/models/hr_lite/hiring_campaign.rb +21 -0
  13. data/app/models/hr_lite/hiring_candidate.rb +52 -0
  14. data/app/models/hr_lite/hiring_interview.rb +13 -0
  15. data/app/models/hr_lite/hiring_message.rb +14 -0
  16. data/app/models/hr_lite/hiring_template.rb +11 -0
  17. data/app/services/hr_lite/hiring_import.rb +106 -0
  18. data/app/services/hr_lite/hiring_merge.rb +48 -0
  19. data/app/views/hr_lite/admin/hiring_campaigns/_form.html.erb +18 -0
  20. data/app/views/hr_lite/admin/hiring_campaigns/edit.html.erb +3 -0
  21. data/app/views/hr_lite/admin/hiring_campaigns/import.html.erb +18 -0
  22. data/app/views/hr_lite/admin/hiring_campaigns/index.html.erb +21 -0
  23. data/app/views/hr_lite/admin/hiring_campaigns/new.html.erb +3 -0
  24. data/app/views/hr_lite/admin/hiring_campaigns/preview.html.erb +62 -0
  25. data/app/views/hr_lite/admin/hiring_campaigns/show.html.erb +98 -0
  26. data/app/views/hr_lite/admin/hiring_candidates/show.html.erb +145 -0
  27. data/app/views/hr_lite/admin/hiring_sends/new.html.erb +34 -0
  28. data/app/views/hr_lite/admin/hiring_templates/_form.html.erb +16 -0
  29. data/app/views/hr_lite/admin/hiring_templates/edit.html.erb +40 -0
  30. data/app/views/hr_lite/admin/hiring_templates/index.html.erb +15 -0
  31. data/app/views/hr_lite/admin/hiring_templates/new.html.erb +3 -0
  32. data/config/routes.rb +10 -0
  33. data/db/migrate/20261021100000_create_hr_lite_hiring_campaigns.rb +15 -0
  34. data/db/migrate/20261021100100_create_hr_lite_hiring_candidates.rb +22 -0
  35. data/db/migrate/20261021100200_create_hr_lite_hiring_templates.rb +10 -0
  36. data/db/migrate/20261021100300_create_hr_lite_hiring_messages.rb +19 -0
  37. data/db/migrate/20261021100400_create_hr_lite_hiring_interviews.rb +16 -0
  38. data/db/migrate/20261021100500_seed_hr_lite_hiring.rb +43 -0
  39. data/lib/hr_lite/configuration.rb +14 -1
  40. data/lib/hr_lite/permissions.rb +2 -1
  41. data/lib/hr_lite/role_seeds.rb +2 -2
  42. data/lib/hr_lite/version.rb +1 -1
  43. data/lib/hr_lite.rb +6 -0
  44. metadata +33 -1
@@ -0,0 +1,15 @@
1
+ <% content_for(:page_title) { "Hiring emails" } %>
2
+ <div class="hrl-page__head">
3
+ <h1 class="hrl-page__title">Hiring email templates</h1>
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
+ </div>
6
+ <section class="hrl-card">
7
+ <ul class="hrl-feed">
8
+ <% @templates.each do |template| %>
9
+ <li class="hrl-feed__item"><div class="hrl-feed__body">
10
+ <a class="hrl-card__link" href="<%= hr_lite.edit_admin_hiring_template_path(template) %>"><%= template.name %></a>
11
+ <span class="hrl-feed__meta"><%= template.subject %></span>
12
+ </div></li>
13
+ <% end %>
14
+ </ul>
15
+ </section>
@@ -0,0 +1,3 @@
1
+ <% content_for(:page_title) { "New template" } %>
2
+ <div class="hrl-page__head"><h1 class="hrl-page__title">New hiring email</h1></div>
3
+ <section class="hrl-card"><%= render "form", template: @template %></section>
data/config/routes.rb CHANGED
@@ -71,6 +71,16 @@ HrLite::Engine.routes.draw do
71
71
 
72
72
  namespace :admin do
73
73
  resources :policies, only: %i[index new create]
74
+ resources :hiring_campaigns, except: %i[destroy] do
75
+ member { get :import; post :import }
76
+ resources :hiring_sends, path: "send", only: %i[new create]
77
+ end
78
+ resources :hiring_candidates, only: %i[show] do
79
+ member { patch :stage }
80
+ resources :hiring_interviews, path: "interviews", only: %i[create update destroy]
81
+ end
82
+ resources :hiring_templates, except: %i[show destroy]
83
+ post "hiring_messages/:id/retry", to: "hiring_sends#retry", as: :retry_hiring_message
74
84
  get "overview", to: "overview#index"
75
85
  resources :attendances, only: %i[index show update], param: :user_id do
76
86
  collection { get :week }
@@ -0,0 +1,15 @@
1
+ class CreateHrLiteHiringCampaigns < ActiveRecord::Migration[8.1]
2
+ def change
3
+ create_table :hr_lite_hiring_campaigns do |t|
4
+ t.string :title, null: false
5
+ t.string :position, null: false
6
+ t.string :programme
7
+ t.string :stipend
8
+ t.string :ctc
9
+ t.string :location
10
+ t.string :schedule
11
+ t.string :status, null: false, default: "open"
12
+ t.timestamps
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ class CreateHrLiteHiringCandidates < ActiveRecord::Migration[8.1]
2
+ def change
3
+ create_table :hr_lite_hiring_candidates do |t|
4
+ t.references :campaign, null: false, foreign_key: { to_table: :hr_lite_hiring_campaigns }
5
+ t.string :name, null: false
6
+ t.string :email, null: false
7
+ t.text :phone
8
+ t.text :date_of_birth
9
+ t.string :campus
10
+ t.string :course
11
+ t.string :branch
12
+ t.string :cv_url
13
+ t.json :extra, null: false, default: {}
14
+ t.string :stage, null: false, default: "imported"
15
+ t.integer :round, null: false, default: 0
16
+ t.string :rejection_reason
17
+ t.timestamps
18
+ end
19
+ add_index :hr_lite_hiring_candidates, %i[campaign_id email], unique: true
20
+ add_index :hr_lite_hiring_candidates, %i[campaign_id stage]
21
+ end
22
+ end
@@ -0,0 +1,10 @@
1
+ class CreateHrLiteHiringTemplates < ActiveRecord::Migration[8.1]
2
+ def change
3
+ create_table :hr_lite_hiring_templates do |t|
4
+ t.string :name, null: false
5
+ t.string :subject, null: false
6
+ t.text :body, null: false
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ class CreateHrLiteHiringMessages < ActiveRecord::Migration[8.1]
2
+ def change
3
+ create_table :hr_lite_hiring_messages do |t|
4
+ t.references :candidate, null: false, foreign_key: { to_table: :hr_lite_hiring_candidates }
5
+ t.references :template, null: false, foreign_key: { to_table: :hr_lite_hiring_templates }
6
+ t.string :batch, null: false
7
+ t.string :status, null: false, default: "queued"
8
+ t.string :gmail_message_id
9
+ t.string :gmail_thread_id
10
+ t.string :error
11
+ t.datetime :sent_at
12
+ t.bigint :sent_by_id
13
+ t.timestamps
14
+ end
15
+ # One send of a template per candidate: double clicks and job redelivery cannot repeat it.
16
+ add_index :hr_lite_hiring_messages, %i[candidate_id template_id], unique: true
17
+ add_index :hr_lite_hiring_messages, :batch
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ class CreateHrLiteHiringInterviews < ActiveRecord::Migration[8.1]
2
+ def change
3
+ create_table :hr_lite_hiring_interviews do |t|
4
+ t.references :candidate, null: false, foreign_key: { to_table: :hr_lite_hiring_candidates }
5
+ t.integer :round, null: false
6
+ t.datetime :starts_at, null: false
7
+ t.datetime :ends_at, null: false
8
+ t.string :status, null: false, default: "scheduled"
9
+ t.string :event_id
10
+ t.string :meet_link
11
+ t.string :error
12
+ t.timestamps
13
+ end
14
+ add_index :hr_lite_hiring_interviews, %i[candidate_id round]
15
+ end
16
+ end
@@ -0,0 +1,43 @@
1
+ class SeedHrLiteHiring < ActiveRecord::Migration[8.1]
2
+ # hiring.manage is new in 0.61.0. RoleSeeds only creates missing roles, so
3
+ # the existing HR, Leadership and Super Admin roles get it here. Also seeds
4
+ # the first outreach email so HR starts from the approved wording.
5
+ BODY = <<~TEXT.freeze
6
+ Hi @first_name,
7
+
8
+ I hope you're doing well.
9
+
10
+ I've gone through your profile and CV, and I believe your experience could be a good fit for the @position position at escapes.asia
11
+
12
+ Before proceeding with the interview process, we'd like to confirm your interest and comfort with the following details:
13
+
14
+ Position: @position
15
+ Programme: @programme
16
+ Monthly Stipend: @stipend
17
+ Full-Time CTC: @ctc, subject to performance and successful completion of the programme
18
+ Work Location: @location
19
+ Work Schedule: @schedule
20
+
21
+ The programme includes structured training during the first three months, followed by performance reviews and continued hands-on experience. Progression to a full-time role is merit-based and depends on performance throughout the programme.
22
+
23
+ Please reply to confirm your interest and availability. Once we receive your confirmation, we'll share the interview schedule and further details.
24
+
25
+ Looking forward to hearing from you.
26
+
27
+ Best,
28
+ @sender_name
29
+ TEXT
30
+
31
+ def up
32
+ HrLite::Role.where(name: [ "HR", "Leadership", "Super Admin" ]).find_each do |role|
33
+ HrLite::RoleGrant.find_or_initialize_by(role: role, permission_key: "hiring.manage").update!(scope: "all")
34
+ end
35
+ execute <<~SQL
36
+ INSERT INTO hr_lite_hiring_templates (name, subject, body, created_at, updated_at)
37
+ VALUES (#{connection.quote("Interest check")}, #{connection.quote("@position at escapes.asia: please confirm your interest")},
38
+ #{connection.quote(BODY)}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
39
+ SQL
40
+ end
41
+
42
+ def down; end
43
+ end
@@ -35,7 +35,13 @@ module HrLite
35
35
  # ->(user, query) { [{ id:, label: }] } — leads to link to a client meeting
36
36
  :crm_search,
37
37
  # ->(id) { { label:, url: } or nil } — one lead, to validate and link it
38
- :crm_find
38
+ :crm_find,
39
+ # An object with sender, send_email, create_event, update_event and
40
+ # cancel_event (see README, Hiring). nil = hiring cannot send or schedule.
41
+ :hiring,
42
+ # ->(path, filename) { rows as arrays, headers first } — reads an
43
+ # uploaded candidate sheet. Default: .csv only.
44
+ :read_sheet
39
45
 
40
46
  attr_reader :leave_year_start_month, :slip_release_day, :self_regularization, :work_hours
41
47
 
@@ -124,6 +130,13 @@ module HrLite
124
130
  @require_punch_location = false
125
131
  @attendance_slots = false
126
132
  @office_geofence = false
133
+ @hiring = nil
134
+ @read_sheet = ->(path, filename) {
135
+ raise ArgumentError, "Upload the sheet as a .csv file." unless filename.to_s.downcase.end_with?(".csv")
136
+
137
+ require "csv"
138
+ CSV.read(path, encoding: "bom|utf-8")
139
+ }
127
140
  @work_hours = nil # e.g. { day: 8, probation_day: 9, week: 40 } — short days close as half days
128
141
 
129
142
  # Leadership onboarding/offboarding. onboard_user must return a saved
@@ -65,7 +65,8 @@ module HrLite
65
65
  "settings.manage" => [ "Administration", "Change company settings, offices and policy" ],
66
66
  "audit.view" => [ "Administration", "Read the audit trail" ],
67
67
  "audit.view_money" => [ "Administration", "Read audit rows about pay, appraisals and promotions" ],
68
- "role.manage" => [ "Administration", "Create roles and grant permissions" ]
68
+ "role.manage" => [ "Administration", "Create roles and grant permissions" ],
69
+ "hiring.manage" => [ "Hiring", "Import candidates, email them from the hiring mailbox, schedule interviews and decide" ]
69
70
  }.freeze
70
71
 
71
72
  KEYS = REGISTRY.keys.freeze
@@ -42,7 +42,7 @@ module HrLite
42
42
  "expense.claim" => "self", "benefit.view" => "all", "benefit.manage" => "all",
43
43
  "hr_request.raise" => "self", "hr_request.manage" => "all", "policy.view" => "all",
44
44
  "asset.view" => "all", "asset.manage" => "all", "checklist.manage" => "all",
45
- "work_analytics.view" => "all"
45
+ "work_analytics.view" => "all", "hiring.manage" => "all"
46
46
  }
47
47
  },
48
48
  Role::FINANCE => {
@@ -69,7 +69,7 @@ module HrLite
69
69
  "document.view" => "all", "expense.approve" => "all", "benefit.manage" => "all",
70
70
  "hr_request.manage" => "all", "policy.view" => "all", "policy.manage" => "all",
71
71
  "asset.view" => "all", "asset.manage" => "all", "checklist.manage" => "all",
72
- "work_analytics.view" => "all"
72
+ "work_analytics.view" => "all", "hiring.manage" => "all"
73
73
  }
74
74
  },
75
75
  Role::CA => {
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.60.0"
2
+ VERSION = "0.61.0"
3
3
  end
data/lib/hr_lite.rb CHANGED
@@ -17,6 +17,12 @@ require "hr_lite/amount_in_words"
17
17
  require "hr_lite/statutory_rate_card"
18
18
 
19
19
  module HrLite
20
+ # Raised by a host hiring adapter. Failed: Google refused, nothing was sent.
21
+ # Unknown: Google may or may not have acted (a timeout, a 5xx), so the row is
22
+ # marked for a person to check and never retried by itself.
23
+ HiringDeliveryFailed = Class.new(StandardError)
24
+ HiringDeliveryUnknown = Class.new(StandardError)
25
+
20
26
  # Full day / first half / second half — one picker for leave and attendance.
21
27
  DAY_PARTS = [ [ "Full day", "" ], [ "First half", "first" ], [ "Second half", "second" ] ].freeze
22
28
  class << self
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.60.0
4
+ version: 0.61.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kshitiz sinha
@@ -97,6 +97,11 @@ files:
97
97
  - app/controllers/hr_lite/admin/employees_controller.rb
98
98
  - app/controllers/hr_lite/admin/expense_categories_controller.rb
99
99
  - app/controllers/hr_lite/admin/expenses_controller.rb
100
+ - app/controllers/hr_lite/admin/hiring_campaigns_controller.rb
101
+ - app/controllers/hr_lite/admin/hiring_candidates_controller.rb
102
+ - app/controllers/hr_lite/admin/hiring_interviews_controller.rb
103
+ - app/controllers/hr_lite/admin/hiring_sends_controller.rb
104
+ - app/controllers/hr_lite/admin/hiring_templates_controller.rb
100
105
  - app/controllers/hr_lite/admin/holidays_controller.rb
101
106
  - app/controllers/hr_lite/admin/hr_requests_controller.rb
102
107
  - app/controllers/hr_lite/admin/leadership_controller.rb
@@ -168,6 +173,7 @@ files:
168
173
  - app/jobs/hr_lite/calendar_meeting_sync_job.rb
169
174
  - app/jobs/hr_lite/daily_digest_job.rb
170
175
  - app/jobs/hr_lite/document_expiry_job.rb
176
+ - app/jobs/hr_lite/hiring_send_job.rb
171
177
  - app/jobs/hr_lite/leave_year_rollover_job.rb
172
178
  - app/jobs/hr_lite/payroll_auto_approve_job.rb
173
179
  - app/jobs/hr_lite/payroll_auto_draft_job.rb
@@ -205,6 +211,11 @@ files:
205
211
  - app/models/hr_lite/employee_profile.rb
206
212
  - app/models/hr_lite/expense.rb
207
213
  - app/models/hr_lite/expense_category.rb
214
+ - app/models/hr_lite/hiring_campaign.rb
215
+ - app/models/hr_lite/hiring_candidate.rb
216
+ - app/models/hr_lite/hiring_interview.rb
217
+ - app/models/hr_lite/hiring_message.rb
218
+ - app/models/hr_lite/hiring_template.rb
208
219
  - app/models/hr_lite/holiday.rb
209
220
  - app/models/hr_lite/hr_request.rb
210
221
  - app/models/hr_lite/kudo.rb
@@ -248,6 +259,8 @@ files:
248
259
  - app/services/hr_lite/calculators/proration.rb
249
260
  - app/services/hr_lite/calculators/tds.rb
250
261
  - app/services/hr_lite/day_status.rb
262
+ - app/services/hr_lite/hiring_import.rb
263
+ - app/services/hr_lite/hiring_merge.rb
251
264
  - app/services/hr_lite/leave_day_counter.rb
252
265
  - app/services/hr_lite/overview_query.rb
253
266
  - app/services/hr_lite/payroll_run_processor.rb
@@ -296,6 +309,19 @@ files:
296
309
  - app/views/hr_lite/admin/expense_categories/index.html.erb
297
310
  - app/views/hr_lite/admin/expense_categories/new.html.erb
298
311
  - app/views/hr_lite/admin/expenses/index.html.erb
312
+ - app/views/hr_lite/admin/hiring_campaigns/_form.html.erb
313
+ - app/views/hr_lite/admin/hiring_campaigns/edit.html.erb
314
+ - app/views/hr_lite/admin/hiring_campaigns/import.html.erb
315
+ - app/views/hr_lite/admin/hiring_campaigns/index.html.erb
316
+ - app/views/hr_lite/admin/hiring_campaigns/new.html.erb
317
+ - app/views/hr_lite/admin/hiring_campaigns/preview.html.erb
318
+ - app/views/hr_lite/admin/hiring_campaigns/show.html.erb
319
+ - app/views/hr_lite/admin/hiring_candidates/show.html.erb
320
+ - app/views/hr_lite/admin/hiring_sends/new.html.erb
321
+ - app/views/hr_lite/admin/hiring_templates/_form.html.erb
322
+ - app/views/hr_lite/admin/hiring_templates/edit.html.erb
323
+ - app/views/hr_lite/admin/hiring_templates/index.html.erb
324
+ - app/views/hr_lite/admin/hiring_templates/new.html.erb
299
325
  - app/views/hr_lite/admin/holidays/index.html.erb
300
326
  - app/views/hr_lite/admin/hr_requests/index.html.erb
301
327
  - app/views/hr_lite/admin/hr_requests/show.html.erb
@@ -508,6 +534,12 @@ files:
508
534
  - db/migrate/20261016100000_add_hours_allowance_repeat_to_hr_lite_employee_profiles.rb
509
535
  - db/migrate/20261017161441_make_hr_lite_benefit_enrolments_unique_while_open.rb
510
536
  - db/migrate/20261017161443_add_attendance_record_id_index_to_hr_lite_work_mode_events.rb
537
+ - db/migrate/20261021100000_create_hr_lite_hiring_campaigns.rb
538
+ - db/migrate/20261021100100_create_hr_lite_hiring_candidates.rb
539
+ - db/migrate/20261021100200_create_hr_lite_hiring_templates.rb
540
+ - db/migrate/20261021100300_create_hr_lite_hiring_messages.rb
541
+ - db/migrate/20261021100400_create_hr_lite_hiring_interviews.rb
542
+ - db/migrate/20261021100500_seed_hr_lite_hiring.rb
511
543
  - lib/generators/hr_lite/install/install_generator.rb
512
544
  - lib/generators/hr_lite/install/templates/AFTER_INSTALL
513
545
  - lib/generators/hr_lite/install/templates/initializer.rb