hr_lite 0.30.0 → 0.35.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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/app/assets/javascripts/hr_lite/chart_tip.js +33 -0
  4. data/app/assets/javascripts/hr_lite/countdown.js +12 -0
  5. data/app/assets/javascripts/hr_lite/crm_picker.js +27 -0
  6. data/app/assets/javascripts/hr_lite/emoji_pick.js +11 -0
  7. data/app/assets/stylesheets/hr_lite/hr_lite.css +47 -0
  8. data/app/controllers/hr_lite/admin/attendances_controller.rb +8 -5
  9. data/app/controllers/hr_lite/admin/employees_controller.rb +1 -1
  10. data/app/controllers/hr_lite/admin/holidays_controller.rb +17 -9
  11. data/app/controllers/hr_lite/admin/settings_controller.rb +1 -1
  12. data/app/controllers/hr_lite/admin/wfh_requests_controller.rb +103 -0
  13. data/app/controllers/hr_lite/admin/work_locations_controller.rb +41 -0
  14. data/app/controllers/hr_lite/admin/work_modes_controller.rb +72 -0
  15. data/app/controllers/hr_lite/admin/work_time_controller.rb +39 -0
  16. data/app/controllers/hr_lite/attendance_controller.rb +2 -1
  17. data/app/controllers/hr_lite/calendar_controller.rb +3 -2
  18. data/app/controllers/hr_lite/comp_off_requests_controller.rb +1 -1
  19. data/app/controllers/hr_lite/holidays_controller.rb +3 -2
  20. data/app/controllers/hr_lite/team_controller.rb +13 -0
  21. data/app/controllers/hr_lite/wfh_requests_controller.rb +45 -0
  22. data/app/controllers/hr_lite/work_locations_controller.rb +44 -0
  23. data/app/controllers/hr_lite/work_modes_controller.rb +40 -0
  24. data/app/controllers/hr_lite/work_time_controller.rb +14 -0
  25. data/app/helpers/hr_lite/application_helper.rb +43 -2
  26. data/app/jobs/hr_lite/attendance_close_job.rb +12 -5
  27. data/app/jobs/hr_lite/calendar_meeting_start_job.rb +31 -0
  28. data/app/jobs/hr_lite/calendar_meeting_sync_job.rb +55 -0
  29. data/app/jobs/hr_lite/work_mode_expire_job.rb +12 -0
  30. data/app/models/hr_lite/attendance_penalty.rb +2 -2
  31. data/app/models/hr_lite/attendance_record.rb +8 -0
  32. data/app/models/hr_lite/comp_off_request.rb +2 -2
  33. data/app/models/hr_lite/employee_profile.rb +2 -0
  34. data/app/models/hr_lite/holiday.rb +11 -5
  35. data/app/models/hr_lite/leave_balance.rb +1 -1
  36. data/app/models/hr_lite/leave_request.rb +1 -1
  37. data/app/models/hr_lite/regularization_request.rb +1 -1
  38. data/app/models/hr_lite/setting.rb +2 -0
  39. data/app/models/hr_lite/wfh_request.rb +108 -0
  40. data/app/models/hr_lite/work_location.rb +62 -0
  41. data/app/models/hr_lite/work_mode_event.rb +84 -0
  42. data/app/services/hr_lite/attendance_puncher.rb +79 -4
  43. data/app/services/hr_lite/day_status.rb +1 -1
  44. data/app/services/hr_lite/leave_day_counter.rb +5 -7
  45. data/app/services/hr_lite/team_day.rb +6 -6
  46. data/app/services/hr_lite/today_checkins.rb +41 -0
  47. data/app/services/hr_lite/work_mode_durations.rb +237 -0
  48. data/app/services/hr_lite/work_mode_switch.rb +144 -0
  49. data/app/services/hr_lite/work_status.rb +60 -0
  50. data/app/services/hr_lite/working_calendar.rb +11 -2
  51. data/app/views/hr_lite/admin/attendances/show.html.erb +1 -1
  52. data/app/views/hr_lite/admin/employees/_form.html.erb +9 -0
  53. data/app/views/hr_lite/admin/holidays/index.html.erb +10 -1
  54. data/app/views/hr_lite/admin/settings/edit.html.erb +9 -0
  55. data/app/views/hr_lite/admin/shared/_approvals_tabs.html.erb +7 -1
  56. data/app/views/hr_lite/admin/wfh_requests/index.html.erb +91 -0
  57. data/app/views/hr_lite/admin/wfh_requests/show.html.erb +40 -0
  58. data/app/views/hr_lite/admin/work_locations/index.html.erb +42 -0
  59. data/app/views/hr_lite/admin/work_modes/index.html.erb +105 -0
  60. data/app/views/hr_lite/admin/work_time/index.html.erb +42 -0
  61. data/app/views/hr_lite/attendance/_calendar_connection.html.erb +15 -0
  62. data/app/views/hr_lite/attendance/_crm_picker.html.erb +9 -0
  63. data/app/views/hr_lite/attendance/_custom_status.html.erb +26 -0
  64. data/app/views/hr_lite/attendance/_punch_card.html.erb +23 -0
  65. data/app/views/hr_lite/attendance/_work_mode.html.erb +92 -0
  66. data/app/views/hr_lite/attendance/show.html.erb +4 -1
  67. data/app/views/hr_lite/holidays/_next_holiday.html.erb +1 -1
  68. data/app/views/hr_lite/home/_today_checkins.html.erb +31 -0
  69. data/app/views/hr_lite/home/index.html.erb +5 -0
  70. data/app/views/hr_lite/team/show.html.erb +25 -4
  71. data/app/views/hr_lite/wfh_requests/index.html.erb +70 -0
  72. data/app/views/hr_lite/work_locations/index.html.erb +52 -0
  73. data/app/views/hr_lite/work_time/_bars.html.erb +44 -0
  74. data/app/views/hr_lite/work_time/_donut.html.erb +36 -0
  75. data/app/views/hr_lite/work_time/_report.html.erb +74 -0
  76. data/app/views/hr_lite/work_time/_timeline.html.erb +26 -0
  77. data/app/views/hr_lite/work_time/show.html.erb +10 -0
  78. data/config/routes.rb +22 -0
  79. data/db/migrate/20261013100000_create_hr_lite_wfh_requests.rb +22 -0
  80. data/db/migrate/20261013100100_add_wfh_request_id_to_hr_lite_attendance_records.rb +7 -0
  81. data/db/migrate/20261013100200_add_permanent_wfh_to_hr_lite_employee_profiles.rb +5 -0
  82. data/db/migrate/20261013110000_create_hr_lite_work_locations.rb +23 -0
  83. data/db/migrate/20261013110100_add_work_mode_to_hr_lite_attendance_records.rb +7 -0
  84. data/db/migrate/20261013110200_add_field_staff_to_hr_lite_employee_profiles.rb +5 -0
  85. data/db/migrate/20261013110300_create_hr_lite_work_mode_events.rb +30 -0
  86. data/db/migrate/20261013110400_add_break_minutes_to_hr_lite_settings.rb +6 -0
  87. data/db/migrate/20261013120000_grant_hr_lite_work_analytics.rb +14 -0
  88. data/db/migrate/20261013130000_add_office_location_to_hr_lite_holidays.rb +12 -0
  89. data/db/migrate/20261013130100_add_office_location_to_hr_lite_employee_profiles.rb +6 -0
  90. data/db/migrate/20261013140000_add_calendar_and_crm_to_hr_lite_work_mode_events.rb +9 -0
  91. data/db/migrate/20261013140100_add_custom_status_to_hr_lite_work_mode_events.rb +9 -0
  92. data/lib/hr_lite/configuration.rb +16 -1
  93. data/lib/hr_lite/notifications.rb +13 -4
  94. data/lib/hr_lite/permissions.rb +1 -0
  95. data/lib/hr_lite/role_seeds.rb +4 -2
  96. data/lib/hr_lite/seeds.rb +1 -1
  97. data/lib/hr_lite/version.rb +1 -1
  98. metadata +53 -1
@@ -0,0 +1,74 @@
1
+ <%# locals: result (WorkModeDurations::Result), period, path (->(params) { url }), records, names, keep, timeline (all optional) %>
2
+ <% path = local_assigns[:path]
3
+ records = local_assigns[:records] || []
4
+ names = local_assigns[:names]
5
+ label = ->(m) { HrLite::WorkModeEvent.label(m) || m.humanize } %>
6
+
7
+ <section class="hrl-card">
8
+ <div class="hrl-row hrl-row--tabs hrl-row--mb">
9
+ <% HrLite::WorkModeDurations::Period::KINDS.excluding("custom").each do |kind| %>
10
+ <a class="hrl-btn <%= 'hrl-btn--primary' if period.kind == kind %>" <%= 'aria-current="page"'.html_safe if period.kind == kind %>
11
+ href="<%= path.(period: kind, date: period.from) %>"><%= kind.capitalize %></a>
12
+ <% end %>
13
+ </div>
14
+ <div class="hrl-row hrl-row--mb">
15
+ <% unless period.kind == "custom" %>
16
+ <a class="hrl-btn" href="<%= path.(period: period.kind, date: period.shift(-1)) %>" aria-label="Previous <%= period.kind %>">&lsaquo;</a>
17
+ <% end %>
18
+ <strong><%= period.label %></strong>
19
+ <% if period.kind != "custom" && period.to < Date.current %>
20
+ <a class="hrl-btn" href="<%= path.(period: period.kind, date: period.shift(1)) %>" aria-label="Next <%= period.kind %>">&rsaquo;</a>
21
+ <% end %>
22
+ </div>
23
+ <%= form_with url: path.({}), method: :get, local: true, class: "hrl-row" do %>
24
+ <input type="hidden" name="period" value="custom">
25
+ <% (local_assigns[:keep] || {}).each do |k, v| %><input type="hidden" name="<%= k %>" value="<%= v %>"><% end %>
26
+ <div class="hrl-field"><label for="wt-from">From</label><%= date_field_tag :from, period.from, max: Date.current, id: "wt-from" %></div>
27
+ <div class="hrl-field"><label for="wt-to">To</label><%= date_field_tag :to, period.to, max: Date.current, id: "wt-to" %></div>
28
+ <div class="hrl-form-actions"><button type="submit" class="hrl-btn">Show range</button></div>
29
+ <% end %>
30
+ </section>
31
+
32
+ <% if result.days_with_data.empty? %>
33
+ <section class="hrl-card"><p class="hrl-card--empty">No recorded activity in <%= period.label %>. Days with nothing recorded are left out rather than shown as zero.</p></section>
34
+ <% else %>
35
+ <% office = result.by_mode["office"]; wfh = result.by_mode["wfh"]; field = result.by_mode["travelling"] + result.by_mode["outside_meeting"] %>
36
+ <div class="hrl-kpis">
37
+ <% [ [ "Attendance (check-in to check-out)", result.attendance ], [ "Working time", result.working ], [ "Breaks", result.breaks ],
38
+ [ "In office", office ], [ "Work from home", wfh ], [ "Travel and client meetings", field ], [ "Out of office", result.other ] ].each do |kpi_label, secs| %>
39
+ <div class="hrl-kpi"><div class="hrl-kpi__value"><%= hrl_duration(secs) %></div><div class="hrl-kpi__label"><%= kpi_label %></div></div>
40
+ <% end %>
41
+ </div>
42
+ <p class="hrl-small hrl-muted">Working time = in office, work from home, travelling, client and other meetings, and busy. It is time in those modes, not a measure of output.
43
+ Recorded on <%= pluralize(result.days_with_data.size, "day") %>.</p>
44
+
45
+ <%= render "hr_lite/work_time/donut", result: result, label: label %>
46
+ <% if period.kind == "day" %>
47
+ <% if local_assigns.fetch(:timeline, true) %>
48
+ <%= render "hr_lite/work_time/timeline", result: result, period: period, label: label, names: names %>
49
+ <% end %>
50
+ <% else %>
51
+ <%= render "hr_lite/work_time/bars", result: result, period: period, label: label, path: path %>
52
+ <% end %>
53
+
54
+ <% manual = result.by_source["employee"] + result.by_source["hr"] %>
55
+ <section class="hrl-card">
56
+ <h2 class="hrl-card__title">Set automatically vs chosen</h2>
57
+ <p class="hrl-small">Automatic (check-in, check-out, timers): <strong><%= hrl_duration(result.by_source["system"]) %></strong> ·
58
+ chosen by the employee: <strong><%= hrl_duration(result.by_source["employee"]) %></strong>
59
+ <% if result.by_source["hr"].positive? %> · set by HR: <strong><%= hrl_duration(result.by_source["hr"]) %></strong><% end %></p>
60
+ </section>
61
+
62
+ <% if records.any? %>
63
+ <section class="hrl-card">
64
+ <h2 class="hrl-card__title">Check-ins</h2>
65
+ <% records.each do |r| %>
66
+ <div class="hrl-feed__item hrl-small"><div class="hrl-feed__body">
67
+ <% if names %><strong><%= names[r.user_id] %></strong> · <% end %>
68
+ <%= r.date.strftime("%a %-d %b") %> · in <%= r.check_in_at.strftime("%H:%M") %> · out <%= r.check_out_at&.strftime("%H:%M") || "—" %>
69
+ <% if r.work_mode_label %> · <%= r.work_mode_label %><% end %>
70
+ </div></div>
71
+ <% end %>
72
+ </section>
73
+ <% end %>
74
+ <% end %>
@@ -0,0 +1,26 @@
1
+ <%# A day as one horizontal strip per person: 00:00 to 24:00. A dashed edge marks a system-set stretch. %>
2
+ <% day_start = period.from.in_time_zone(HrLite.config.time_zone)
3
+ rows = result.segments.group_by(&:user_id)
4
+ pct = ->(t) { ((t - day_start) / 86_400.0 * 100).clamp(0, 100) } %>
5
+ <section class="hrl-card">
6
+ <h2 class="hrl-card__title">The day, hour by hour</h2>
7
+ <% rows.each do |user_id, segs| %>
8
+ <% if names %><p class="hrl-small"><strong><%= names[user_id] %></strong></p><% end %>
9
+ <svg class="hrl-timeline" viewBox="0 0 100 10" preserveAspectRatio="none" role="img" aria-label="Work modes through the day; details in the list below">
10
+ <% segs.each do |seg| %>
11
+ <rect x="<%= pct.(seg.from).round(3) %>" y="1" width="<%= [ pct.(seg.to) - pct.(seg.from), 0.15 ].max.round(3) %>" height="8"
12
+ class="<%= class_names("hrl-fill--#{seg.mode}", "hrl-timeline__auto": seg.source == "system") %>" data-hrl-tip tabindex="0">
13
+ <title><%= label.(seg.mode) %> · <%= seg.from.strftime("%H:%M") %>–<%= seg.to.strftime("%H:%M") %> · <%= hrl_duration(seg.seconds) %> · <%= HrLite::ApplicationHelper::MODE_SOURCES[seg.source] %></title>
14
+ </rect>
15
+ <% end %>
16
+ </svg>
17
+ <div class="hrl-timeline__axis hrl-small hrl-muted" aria-hidden="true"><span>00</span><span>06</span><span>12</span><span>18</span><span>24</span></div>
18
+ <ol class="hrl-timeline__list hrl-small">
19
+ <% segs.each do |seg| %>
20
+ <li><span class="hrl-swatch hrl-fill-bg--<%= seg.mode %>" aria-hidden="true"></span>
21
+ <strong><%= seg.from.strftime("%H:%M") %>–<%= seg.to.strftime("%H:%M") %></strong> <%= label.(seg.mode) %> · <%= hrl_duration(seg.seconds) %>
22
+ <span class="hrl-muted">· <%= HrLite::ApplicationHelper::MODE_SOURCES[seg.source] %></span></li>
23
+ <% end %>
24
+ </ol>
25
+ <% end %>
26
+ </section>
@@ -0,0 +1,10 @@
1
+ <% content_for(:page_title) { "My work time" } %>
2
+ <% content_for(:back, "Attendance") %>
3
+ <div class="hrl-page__head">
4
+ <h1 class="hrl-page__title">My work time</h1>
5
+ </div>
6
+ <p class="hrl-small hrl-muted">Only you and HR can see this page's detail. Times come from your recorded check-ins and work modes.</p>
7
+ <% if @period_error %><div class="hrl-errors" role="alert"><%= @period_error %></div><% end %>
8
+ <%= render "hr_lite/work_time/report", result: @result, period: @period, records: @records,
9
+ path: ->(p) { hr_lite.work_time_path(p) } %>
10
+ <% content_for :scripts do %><%= javascript_include_tag "hr_lite/chart_tip", defer: true %><% end %>
data/config/routes.rb CHANGED
@@ -20,6 +20,19 @@ HrLite::Engine.routes.draw do
20
20
  resources :regularization_requests, only: %i[index new create] do
21
21
  member { post :cancel }
22
22
  end
23
+ resources :wfh_requests, only: %i[index create] do
24
+ member { post :cancel }
25
+ end
26
+ get "work_time", to: "work_time#show", as: :work_time
27
+ resource :work_mode, only: :create do
28
+ post :end_overlay
29
+ get :crm_search
30
+ post :status
31
+ post :clear_status
32
+ end
33
+ resources :work_locations, only: %i[index create] do
34
+ member { post :activate; post :remove }
35
+ end
23
36
  resources :approvals, only: :index
24
37
  resources :expenses, only: %i[index new create] do
25
38
  member { post :cancel }
@@ -74,6 +87,15 @@ HrLite::Engine.routes.draw do
74
87
  resources :regularization_requests, only: %i[index show] do
75
88
  member { post :approve; post :reject }
76
89
  end
90
+ resources :wfh_requests, only: %i[index show create] do
91
+ member { post :approve; post :reject }
92
+ end
93
+ resources :work_locations, only: :index do
94
+ member { post :approve; post :reject }
95
+ end
96
+ get "work_modes", to: "work_modes#index", as: :work_modes
97
+ get "work_time", to: "work_time#index", as: :work_time
98
+ post "work_modes/:user_id", to: "work_modes#override", as: :work_mode_override
77
99
  resources :roles, except: :show do
78
100
  resources :assignments, only: %i[create destroy], controller: "role_assignments"
79
101
  end
@@ -0,0 +1,22 @@
1
+ class CreateHrLiteWfhRequests < ActiveRecord::Migration[8.1]
2
+ def change
3
+ create_table :hr_lite_wfh_requests do |t|
4
+ t.bigint :user_id, null: false
5
+ t.date :start_date, null: false
6
+ t.date :end_date, null: false
7
+ t.text :reason, null: false
8
+ t.string :status, null: false, default: "pending"
9
+ t.bigint :decided_by_id
10
+ t.datetime :decided_at
11
+ t.text :decision_note
12
+ t.bigint :created_by_id
13
+
14
+ t.timestamps
15
+ end
16
+ add_index :hr_lite_wfh_requests, [ :user_id, :start_date, :end_date ]
17
+ add_index :hr_lite_wfh_requests, [ :status, :start_date ]
18
+ add_check_constraint :hr_lite_wfh_requests, "status IN ('pending','approved','rejected','cancelled')",
19
+ name: "hr_lite_wfh_requests_status"
20
+ add_check_constraint :hr_lite_wfh_requests, "end_date >= start_date", name: "hr_lite_wfh_requests_range"
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ class AddWfhRequestIdToHrLiteAttendanceRecords < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :hr_lite_attendance_records, :wfh_request_id, :bigint
4
+ add_index :hr_lite_attendance_records, :wfh_request_id
5
+ add_column :hr_lite_attendance_records, :wfh, :boolean, null: false, default: false
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ class AddPermanentWfhToHrLiteEmployeeProfiles < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :hr_lite_employee_profiles, :permanent_wfh, :boolean, null: false, default: false
4
+ end
5
+ end
@@ -0,0 +1,23 @@
1
+ class CreateHrLiteWorkLocations < ActiveRecord::Migration[8.1]
2
+ def change
3
+ create_table :hr_lite_work_locations do |t|
4
+ t.bigint :user_id, null: false
5
+ t.string :name, null: false
6
+ t.decimal :lat, precision: 9, scale: 6, null: false
7
+ t.decimal :lng, precision: 9, scale: 6, null: false
8
+ t.integer :accuracy_m, null: false
9
+ t.string :address
10
+ t.string :status, null: false, default: "pending"
11
+ t.boolean :active, null: false, default: false
12
+ t.bigint :decided_by_id
13
+ t.datetime :decided_at
14
+ t.text :decision_note
15
+
16
+ t.timestamps
17
+ end
18
+ add_index :hr_lite_work_locations, [ :user_id, :status ]
19
+ add_index :hr_lite_work_locations, :user_id, unique: true, where: "active", name: "index_hr_lite_work_locations_one_active"
20
+ add_check_constraint :hr_lite_work_locations, "status IN ('pending','approved','rejected','removed')",
21
+ name: "hr_lite_work_locations_status"
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ class AddWorkModeToHrLiteAttendanceRecords < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :hr_lite_attendance_records, :work_mode, :string
4
+ add_column :hr_lite_attendance_records, :work_location_id, :bigint
5
+ remove_column :hr_lite_attendance_records, :wfh, :boolean, null: false, default: false
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ class AddFieldStaffToHrLiteEmployeeProfiles < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :hr_lite_employee_profiles, :field_staff, :boolean, null: false, default: false
4
+ end
5
+ end
@@ -0,0 +1,30 @@
1
+ class CreateHrLiteWorkModeEvents < ActiveRecord::Migration[8.1]
2
+ def change
3
+ create_table :hr_lite_work_mode_events do |t|
4
+ t.bigint :user_id, null: false
5
+ t.string :mode, null: false
6
+ t.string :layer, null: false
7
+ t.string :source, null: false
8
+ t.datetime :started_at, null: false
9
+ t.datetime :ends_at
10
+ t.datetime :ended_at
11
+ t.string :end_reason
12
+ t.boolean :overrun, null: false, default: false
13
+ t.bigint :attendance_record_id
14
+ t.bigint :wfh_request_id
15
+ t.bigint :work_location_id
16
+ t.bigint :set_by_id
17
+ t.string :title
18
+ t.string :client
19
+ t.string :destination
20
+ t.text :reason
21
+
22
+ t.timestamps
23
+ end
24
+ add_index :hr_lite_work_mode_events, [ :user_id, :started_at ]
25
+ add_index :hr_lite_work_mode_events, [ :user_id, :layer ], unique: true, where: "ended_at IS NULL",
26
+ name: "index_hr_lite_work_mode_events_one_open"
27
+ add_check_constraint :hr_lite_work_mode_events, "source IN ('system','employee','hr')", name: "hr_lite_work_mode_events_source"
28
+ add_check_constraint :hr_lite_work_mode_events, "layer IN ('primary','overlay')", name: "hr_lite_work_mode_events_layer"
29
+ end
30
+ end
@@ -0,0 +1,6 @@
1
+ class AddBreakMinutesToHrLiteSettings < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :hr_lite_settings, :lunch_break_minutes, :integer, null: false, default: 30
4
+ add_column :hr_lite_settings, :short_break_minutes, :integer, null: false, default: 10
5
+ end
6
+ end
@@ -0,0 +1,14 @@
1
+ class GrantHrLiteWorkAnalytics < ActiveRecord::Migration[8.1]
2
+ # work_analytics.view is new in 0.31.0. RoleSeeds only creates missing
3
+ # roles, so the existing HR, Leadership and Super Admin roles get it here.
4
+ # Manager deliberately does not.
5
+ def up
6
+ return unless HrLite::Role.exists?
7
+
8
+ HrLite::Role.where(name: [ "HR", "Leadership", "Super Admin" ]).find_each do |role|
9
+ HrLite::RoleGrant.find_or_initialize_by(role: role, permission_key: "work_analytics.view").update!(scope: "all")
10
+ end
11
+ end
12
+
13
+ def down; end
14
+ end
@@ -0,0 +1,12 @@
1
+ class AddOfficeLocationToHrLiteHolidays < ActiveRecord::Migration[8.1]
2
+ # A holiday is for every office (office_location_id null) or for one; two
3
+ # offices may close on the same date for different reasons.
4
+ def change
5
+ add_column :hr_lite_holidays, :office_location_id, :bigint
6
+ remove_index :hr_lite_holidays, :date, unique: true
7
+ add_index :hr_lite_holidays, :date, unique: true, where: "office_location_id IS NULL",
8
+ name: "index_hr_lite_holidays_all_offices"
9
+ add_index :hr_lite_holidays, [ :date, :office_location_id ], unique: true,
10
+ name: "index_hr_lite_holidays_per_office"
11
+ end
12
+ end
@@ -0,0 +1,6 @@
1
+ class AddOfficeLocationToHrLiteEmployeeProfiles < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :hr_lite_employee_profiles, :office_location_id, :bigint
4
+ add_index :hr_lite_employee_profiles, :office_location_id
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ class AddCalendarAndCrmToHrLiteWorkModeEvents < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :hr_lite_work_mode_events, :calendar_event_id, :string
4
+ add_column :hr_lite_work_mode_events, :crm_ref, :string
5
+ # A calendar meeting opens at most once per person, however often we sync.
6
+ add_index :hr_lite_work_mode_events, [ :user_id, :calendar_event_id ], unique: true,
7
+ where: "calendar_event_id IS NOT NULL", name: "index_hr_lite_work_mode_events_one_per_calendar_event"
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class AddCustomStatusToHrLiteWorkModeEvents < ActiveRecord::Migration[8.1]
2
+ # A third layer: a person's own status ("In Goa till Friday" + an emoji),
3
+ # alongside their work mode and any break or meeting.
4
+ def change
5
+ add_column :hr_lite_work_mode_events, :emoji, :string
6
+ remove_check_constraint :hr_lite_work_mode_events, "layer IN ('primary','overlay')", name: "hr_lite_work_mode_events_layer"
7
+ add_check_constraint :hr_lite_work_mode_events, "layer IN ('primary','overlay','status')", name: "hr_lite_work_mode_events_layer"
8
+ end
9
+ end
@@ -18,7 +18,21 @@ module HrLite
18
18
  # true = a punch without GPS is refused (default: recorded and flagged)
19
19
  :require_punch_location,
20
20
  # true = check-in slots, late penalties and HR buffers (default off)
21
- :attendance_slots
21
+ :attendance_slots,
22
+ # true = check-in only inside an active office radius, unless WFH is approved for the day
23
+ :office_geofence,
24
+ # ->(user, from, to) { [{ id:, title:, starts_at:, ends_at: }] } — timed,
25
+ # accepted events; starts Meeting mode by itself. Pairs with calendar_users.
26
+ :calendar_events,
27
+ # -> { user ids with a connected calendar }
28
+ :calendar_users,
29
+ # ->(user) { { connected:, label:, connect_url:, disconnect_url: } or nil } —
30
+ # the one-click "Connect Google Calendar" button (nil = no button)
31
+ :calendar_connection,
32
+ # ->(user, query) { [{ id:, label: }] } — leads to link to a client meeting
33
+ :crm_search,
34
+ # ->(id) { { label:, url: } or nil } — one lead, to validate and link it
35
+ :crm_find
22
36
 
23
37
  attr_reader :leave_year_start_month, :slip_release_day, :self_regularization, :work_hours
24
38
 
@@ -105,6 +119,7 @@ module HrLite
105
119
  @self_regularization = nil # e.g. { within_days: 2, per_week: 2 } — employees fix their own missed punch
106
120
  @require_punch_location = false
107
121
  @attendance_slots = false
122
+ @office_geofence = false
108
123
  @work_hours = nil # e.g. { day: 8, probation_day: 9, week: 40 } — short days close as half days
109
124
 
110
125
  # Leadership onboarding/offboarding. onboard_user must return a saved
@@ -68,7 +68,14 @@ module HrLite
68
68
  "regularization.requested" => { bell: true, email: false, leadership_email: true, leadership_bell: false },
69
69
  "regularization.approved" => { bell: true, email: true, leadership_email: true, leadership_bell: false },
70
70
  "regularization.rejected" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
71
- "regularization.cancelled" => { bell: true, email: false, leadership_email: true, leadership_bell: false }
71
+ "regularization.cancelled" => { bell: true, email: false, leadership_email: true, leadership_bell: false },
72
+ "wfh.requested" => { bell: true, email: false, leadership_email: true, leadership_bell: false },
73
+ "wfh.approved" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
74
+ "wfh.rejected" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
75
+ "wfh.cancelled" => { bell: true, email: false, leadership_email: false, leadership_bell: false },
76
+ "work_location.requested" => { bell: true, email: false, leadership_email: false, leadership_bell: false },
77
+ "work_location.approved" => { bell: true, email: true, leadership_email: false, leadership_bell: false },
78
+ "work_location.rejected" => { bell: true, email: true, leadership_email: false, leadership_bell: false }
72
79
  }.freeze
73
80
 
74
81
  class << self
@@ -127,9 +134,11 @@ module HrLite
127
134
  next if user.email.blank?
128
135
 
129
136
  begin
130
- EventMailer.event(to: user.email, subject: title, heading: title,
131
- body: body, lines: lines, path: path,
132
- link_url: link_url).deliver_later
137
+ mail = EventMailer.event(to: user.email, subject: title, heading: title,
138
+ body: body, lines: lines, path: path, link_url: link_url)
139
+ # Busy or in a meeting: the email waits until that ends.
140
+ held = WorkStatus.do_not_disturb_until(user.id) if HrLite.config.office_geofence
141
+ held ? mail.deliver_later(wait_until: held) : mail.deliver_later
133
142
  rescue => e
134
143
  Rails.logger.error("[hr_lite] event email failed: #{e.class}: #{e.message}")
135
144
  end
@@ -30,6 +30,7 @@ module HrLite
30
30
  "profile.manage" => [ "People", "Create and edit employee profiles, onboard and offboard" ],
31
31
  "attendance.view" => [ "Attendance", "See attendance records" ],
32
32
  "attendance.manage" => [ "Attendance", "Correct punches and decide regularization tickets" ],
33
+ "work_analytics.view" => [ "Attendance", "See another person's work-time analytics (time per work mode, day timelines)" ],
33
34
  "leave.request" => [ "Leave", "Apply for leave and comp-off" ],
34
35
  "leave.view" => [ "Leave", "See leave requests and balances" ],
35
36
  "leave.approve" => [ "Leave", "Approve, reject and cancel leave and comp-off" ],
@@ -41,7 +41,8 @@ module HrLite
41
41
  "document.view" => "all", "tax.view" => "all", "tax.manage" => "all",
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
- "asset.view" => "all", "asset.manage" => "all", "checklist.manage" => "all"
44
+ "asset.view" => "all", "asset.manage" => "all", "checklist.manage" => "all",
45
+ "work_analytics.view" => "all"
45
46
  }
46
47
  },
47
48
  Role::FINANCE => {
@@ -67,7 +68,8 @@ module HrLite
67
68
  "settings.manage" => "all", "audit.view" => "all", "payroll.view" => "self",
68
69
  "document.view" => "all", "expense.approve" => "all", "benefit.manage" => "all",
69
70
  "hr_request.manage" => "all", "policy.view" => "all", "policy.manage" => "all",
70
- "asset.view" => "all", "asset.manage" => "all", "checklist.manage" => "all"
71
+ "asset.view" => "all", "asset.manage" => "all", "checklist.manage" => "all",
72
+ "work_analytics.view" => "all"
71
73
  }
72
74
  },
73
75
  Role::CA => {
data/lib/hr_lite/seeds.rb CHANGED
@@ -47,7 +47,7 @@ module HrLite
47
47
  [ Date.new(year, 8, 15), "Independence Day" ],
48
48
  [ Date.new(year, 10, 2), "Gandhi Jayanti" ]
49
49
  ].filter_map do |date, name|
50
- next if Holiday.exists?(date: date)
50
+ next if Holiday.exists?(date: date, office_location_id: nil)
51
51
 
52
52
  Holiday.create!(date: date, name: name)
53
53
  "holiday #{name} #{year}"
@@ -1,3 +1,3 @@
1
1
  module HrLite
2
- VERSION = "0.30.0"
2
+ VERSION = "0.35.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.30.0
4
+ version: 0.35.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kshitiz sinha
@@ -64,7 +64,11 @@ files:
64
64
  - MIT-LICENSE
65
65
  - README.md
66
66
  - Rakefile
67
+ - app/assets/javascripts/hr_lite/chart_tip.js
67
68
  - app/assets/javascripts/hr_lite/confirm.js
69
+ - app/assets/javascripts/hr_lite/countdown.js
70
+ - app/assets/javascripts/hr_lite/crm_picker.js
71
+ - app/assets/javascripts/hr_lite/emoji_pick.js
68
72
  - app/assets/javascripts/hr_lite/geo_punch.js
69
73
  - app/assets/javascripts/hr_lite/mention.js
70
74
  - app/assets/javascripts/hr_lite/nav.js
@@ -115,6 +119,10 @@ files:
115
119
  - app/controllers/hr_lite/admin/statutory_rate_cards_controller.rb
116
120
  - app/controllers/hr_lite/admin/superadmin_controller.rb
117
121
  - app/controllers/hr_lite/admin/tax_declarations_controller.rb
122
+ - app/controllers/hr_lite/admin/wfh_requests_controller.rb
123
+ - app/controllers/hr_lite/admin/work_locations_controller.rb
124
+ - app/controllers/hr_lite/admin/work_modes_controller.rb
125
+ - app/controllers/hr_lite/admin/work_time_controller.rb
118
126
  - app/controllers/hr_lite/application_controller.rb
119
127
  - app/controllers/hr_lite/appraisals_controller.rb
120
128
  - app/controllers/hr_lite/approvals_controller.rb
@@ -143,18 +151,25 @@ files:
143
151
  - app/controllers/hr_lite/team_controller.rb
144
152
  - app/controllers/hr_lite/timelines_controller.rb
145
153
  - app/controllers/hr_lite/users_controller.rb
154
+ - app/controllers/hr_lite/wfh_requests_controller.rb
155
+ - app/controllers/hr_lite/work_locations_controller.rb
156
+ - app/controllers/hr_lite/work_modes_controller.rb
157
+ - app/controllers/hr_lite/work_time_controller.rb
146
158
  - app/helpers/hr_lite/application_helper.rb
147
159
  - app/jobs/hr_lite/application_job.rb
148
160
  - app/jobs/hr_lite/approval_escalation_job.rb
149
161
  - app/jobs/hr_lite/attendance_close_job.rb
150
162
  - app/jobs/hr_lite/ca_review_overdue_job.rb
151
163
  - app/jobs/hr_lite/ca_review_reminder_job.rb
164
+ - app/jobs/hr_lite/calendar_meeting_start_job.rb
165
+ - app/jobs/hr_lite/calendar_meeting_sync_job.rb
152
166
  - app/jobs/hr_lite/daily_digest_job.rb
153
167
  - app/jobs/hr_lite/document_expiry_job.rb
154
168
  - app/jobs/hr_lite/leave_year_rollover_job.rb
155
169
  - app/jobs/hr_lite/payroll_auto_approve_job.rb
156
170
  - app/jobs/hr_lite/payroll_auto_draft_job.rb
157
171
  - app/jobs/hr_lite/slips_ready_job.rb
172
+ - app/jobs/hr_lite/work_mode_expire_job.rb
158
173
  - app/mailers/hr_lite/application_mailer.rb
159
174
  - app/mailers/hr_lite/event_mailer.rb
160
175
  - app/models/concerns/hr_lite/approvable.rb
@@ -212,6 +227,9 @@ files:
212
227
  - app/models/hr_lite/statutory_rate_card_record.rb
213
228
  - app/models/hr_lite/tax_declaration.rb
214
229
  - app/models/hr_lite/tax_declaration_item.rb
230
+ - app/models/hr_lite/wfh_request.rb
231
+ - app/models/hr_lite/work_location.rb
232
+ - app/models/hr_lite/work_mode_event.rb
215
233
  - app/services/hr_lite/access.rb
216
234
  - app/services/hr_lite/approval_route.rb
217
235
  - app/services/hr_lite/attendance_puncher.rb
@@ -230,6 +248,10 @@ files:
230
248
  - app/services/hr_lite/tax_computation.rb
231
249
  - app/services/hr_lite/team_day.rb
232
250
  - app/services/hr_lite/timeline.rb
251
+ - app/services/hr_lite/today_checkins.rb
252
+ - app/services/hr_lite/work_mode_durations.rb
253
+ - app/services/hr_lite/work_mode_switch.rb
254
+ - app/services/hr_lite/work_status.rb
233
255
  - app/services/hr_lite/working_calendar.rb
234
256
  - app/views/hr_lite/admin/appraisals/_form.html.erb
235
257
  - app/views/hr_lite/admin/appraisals/edit.html.erb
@@ -317,13 +339,22 @@ files:
317
339
  - app/views/hr_lite/admin/tax_declarations/overview.html.erb
318
340
  - app/views/hr_lite/admin/tax_declarations/person.html.erb
319
341
  - app/views/hr_lite/admin/tax_declarations/show.html.erb
342
+ - app/views/hr_lite/admin/wfh_requests/index.html.erb
343
+ - app/views/hr_lite/admin/wfh_requests/show.html.erb
344
+ - app/views/hr_lite/admin/work_locations/index.html.erb
345
+ - app/views/hr_lite/admin/work_modes/index.html.erb
346
+ - app/views/hr_lite/admin/work_time/index.html.erb
320
347
  - app/views/hr_lite/appraisals/index.html.erb
321
348
  - app/views/hr_lite/appraisals/show.html.erb
322
349
  - app/views/hr_lite/approvals/index.html.erb
350
+ - app/views/hr_lite/attendance/_calendar_connection.html.erb
351
+ - app/views/hr_lite/attendance/_crm_picker.html.erb
352
+ - app/views/hr_lite/attendance/_custom_status.html.erb
323
353
  - app/views/hr_lite/attendance/_month_grid.html.erb
324
354
  - app/views/hr_lite/attendance/_penalties.html.erb
325
355
  - app/views/hr_lite/attendance/_punch_card.html.erb
326
356
  - app/views/hr_lite/attendance/_slot.html.erb
357
+ - app/views/hr_lite/attendance/_work_mode.html.erb
327
358
  - app/views/hr_lite/attendance/show.html.erb
328
359
  - app/views/hr_lite/benefits/index.html.erb
329
360
  - app/views/hr_lite/calendar/show.html.erb
@@ -340,6 +371,7 @@ files:
340
371
  - app/views/hr_lite/expenses/new.html.erb
341
372
  - app/views/hr_lite/holidays/_next_holiday.html.erb
342
373
  - app/views/hr_lite/holidays/index.html.erb
374
+ - app/views/hr_lite/home/_today_checkins.html.erb
343
375
  - app/views/hr_lite/home/index.html.erb
344
376
  - app/views/hr_lite/hr_requests/index.html.erb
345
377
  - app/views/hr_lite/hr_requests/new.html.erb
@@ -373,6 +405,13 @@ files:
373
405
  - app/views/hr_lite/team/show.html.erb
374
406
  - app/views/hr_lite/timelines/_events.html.erb
375
407
  - app/views/hr_lite/timelines/show.html.erb
408
+ - app/views/hr_lite/wfh_requests/index.html.erb
409
+ - app/views/hr_lite/work_locations/index.html.erb
410
+ - app/views/hr_lite/work_time/_bars.html.erb
411
+ - app/views/hr_lite/work_time/_donut.html.erb
412
+ - app/views/hr_lite/work_time/_report.html.erb
413
+ - app/views/hr_lite/work_time/_timeline.html.erb
414
+ - app/views/hr_lite/work_time/show.html.erb
376
415
  - app/views/layouts/hr_lite/application.html.erb
377
416
  - app/views/layouts/hr_lite/mailer.html.erb
378
417
  - app/views/layouts/hr_lite/mailer.text.erb
@@ -431,6 +470,19 @@ files:
431
470
  - db/migrate/20261013090000_create_hr_lite_attendance_slots.rb
432
471
  - db/migrate/20261013090100_create_hr_lite_attendance_penalties.rb
433
472
  - db/migrate/20261013090200_add_buffer_to_hr_lite_employee_profiles.rb
473
+ - db/migrate/20261013100000_create_hr_lite_wfh_requests.rb
474
+ - db/migrate/20261013100100_add_wfh_request_id_to_hr_lite_attendance_records.rb
475
+ - db/migrate/20261013100200_add_permanent_wfh_to_hr_lite_employee_profiles.rb
476
+ - db/migrate/20261013110000_create_hr_lite_work_locations.rb
477
+ - db/migrate/20261013110100_add_work_mode_to_hr_lite_attendance_records.rb
478
+ - db/migrate/20261013110200_add_field_staff_to_hr_lite_employee_profiles.rb
479
+ - db/migrate/20261013110300_create_hr_lite_work_mode_events.rb
480
+ - db/migrate/20261013110400_add_break_minutes_to_hr_lite_settings.rb
481
+ - db/migrate/20261013120000_grant_hr_lite_work_analytics.rb
482
+ - db/migrate/20261013130000_add_office_location_to_hr_lite_holidays.rb
483
+ - db/migrate/20261013130100_add_office_location_to_hr_lite_employee_profiles.rb
484
+ - db/migrate/20261013140000_add_calendar_and_crm_to_hr_lite_work_mode_events.rb
485
+ - db/migrate/20261013140100_add_custom_status_to_hr_lite_work_mode_events.rb
434
486
  - lib/generators/hr_lite/install/install_generator.rb
435
487
  - lib/generators/hr_lite/install/templates/AFTER_INSTALL
436
488
  - lib/generators/hr_lite/install/templates/initializer.rb