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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/app/assets/javascripts/hr_lite/chart_tip.js +33 -0
- data/app/assets/javascripts/hr_lite/countdown.js +12 -0
- data/app/assets/javascripts/hr_lite/crm_picker.js +27 -0
- data/app/assets/javascripts/hr_lite/emoji_pick.js +11 -0
- data/app/assets/stylesheets/hr_lite/hr_lite.css +47 -0
- data/app/controllers/hr_lite/admin/attendances_controller.rb +8 -5
- data/app/controllers/hr_lite/admin/employees_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/holidays_controller.rb +17 -9
- data/app/controllers/hr_lite/admin/settings_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/wfh_requests_controller.rb +103 -0
- data/app/controllers/hr_lite/admin/work_locations_controller.rb +41 -0
- data/app/controllers/hr_lite/admin/work_modes_controller.rb +72 -0
- data/app/controllers/hr_lite/admin/work_time_controller.rb +39 -0
- data/app/controllers/hr_lite/attendance_controller.rb +2 -1
- data/app/controllers/hr_lite/calendar_controller.rb +3 -2
- data/app/controllers/hr_lite/comp_off_requests_controller.rb +1 -1
- data/app/controllers/hr_lite/holidays_controller.rb +3 -2
- data/app/controllers/hr_lite/team_controller.rb +13 -0
- data/app/controllers/hr_lite/wfh_requests_controller.rb +45 -0
- data/app/controllers/hr_lite/work_locations_controller.rb +44 -0
- data/app/controllers/hr_lite/work_modes_controller.rb +40 -0
- data/app/controllers/hr_lite/work_time_controller.rb +14 -0
- data/app/helpers/hr_lite/application_helper.rb +43 -2
- data/app/jobs/hr_lite/attendance_close_job.rb +12 -5
- data/app/jobs/hr_lite/calendar_meeting_start_job.rb +31 -0
- data/app/jobs/hr_lite/calendar_meeting_sync_job.rb +55 -0
- data/app/jobs/hr_lite/work_mode_expire_job.rb +12 -0
- data/app/models/hr_lite/attendance_penalty.rb +2 -2
- data/app/models/hr_lite/attendance_record.rb +8 -0
- data/app/models/hr_lite/comp_off_request.rb +2 -2
- data/app/models/hr_lite/employee_profile.rb +2 -0
- data/app/models/hr_lite/holiday.rb +11 -5
- data/app/models/hr_lite/leave_balance.rb +1 -1
- data/app/models/hr_lite/leave_request.rb +1 -1
- data/app/models/hr_lite/regularization_request.rb +1 -1
- data/app/models/hr_lite/setting.rb +2 -0
- data/app/models/hr_lite/wfh_request.rb +108 -0
- data/app/models/hr_lite/work_location.rb +62 -0
- data/app/models/hr_lite/work_mode_event.rb +84 -0
- data/app/services/hr_lite/attendance_puncher.rb +79 -4
- data/app/services/hr_lite/day_status.rb +1 -1
- data/app/services/hr_lite/leave_day_counter.rb +5 -7
- data/app/services/hr_lite/team_day.rb +6 -6
- data/app/services/hr_lite/today_checkins.rb +41 -0
- data/app/services/hr_lite/work_mode_durations.rb +237 -0
- data/app/services/hr_lite/work_mode_switch.rb +144 -0
- data/app/services/hr_lite/work_status.rb +60 -0
- data/app/services/hr_lite/working_calendar.rb +11 -2
- data/app/views/hr_lite/admin/attendances/show.html.erb +1 -1
- data/app/views/hr_lite/admin/employees/_form.html.erb +9 -0
- data/app/views/hr_lite/admin/holidays/index.html.erb +10 -1
- data/app/views/hr_lite/admin/settings/edit.html.erb +9 -0
- data/app/views/hr_lite/admin/shared/_approvals_tabs.html.erb +7 -1
- data/app/views/hr_lite/admin/wfh_requests/index.html.erb +91 -0
- data/app/views/hr_lite/admin/wfh_requests/show.html.erb +40 -0
- data/app/views/hr_lite/admin/work_locations/index.html.erb +42 -0
- data/app/views/hr_lite/admin/work_modes/index.html.erb +105 -0
- data/app/views/hr_lite/admin/work_time/index.html.erb +42 -0
- data/app/views/hr_lite/attendance/_calendar_connection.html.erb +15 -0
- data/app/views/hr_lite/attendance/_crm_picker.html.erb +9 -0
- data/app/views/hr_lite/attendance/_custom_status.html.erb +26 -0
- data/app/views/hr_lite/attendance/_punch_card.html.erb +23 -0
- data/app/views/hr_lite/attendance/_work_mode.html.erb +92 -0
- data/app/views/hr_lite/attendance/show.html.erb +4 -1
- data/app/views/hr_lite/holidays/_next_holiday.html.erb +1 -1
- data/app/views/hr_lite/home/_today_checkins.html.erb +31 -0
- data/app/views/hr_lite/home/index.html.erb +5 -0
- data/app/views/hr_lite/team/show.html.erb +25 -4
- data/app/views/hr_lite/wfh_requests/index.html.erb +70 -0
- data/app/views/hr_lite/work_locations/index.html.erb +52 -0
- data/app/views/hr_lite/work_time/_bars.html.erb +44 -0
- data/app/views/hr_lite/work_time/_donut.html.erb +36 -0
- data/app/views/hr_lite/work_time/_report.html.erb +74 -0
- data/app/views/hr_lite/work_time/_timeline.html.erb +26 -0
- data/app/views/hr_lite/work_time/show.html.erb +10 -0
- data/config/routes.rb +22 -0
- data/db/migrate/20261013100000_create_hr_lite_wfh_requests.rb +22 -0
- data/db/migrate/20261013100100_add_wfh_request_id_to_hr_lite_attendance_records.rb +7 -0
- data/db/migrate/20261013100200_add_permanent_wfh_to_hr_lite_employee_profiles.rb +5 -0
- data/db/migrate/20261013110000_create_hr_lite_work_locations.rb +23 -0
- data/db/migrate/20261013110100_add_work_mode_to_hr_lite_attendance_records.rb +7 -0
- data/db/migrate/20261013110200_add_field_staff_to_hr_lite_employee_profiles.rb +5 -0
- data/db/migrate/20261013110300_create_hr_lite_work_mode_events.rb +30 -0
- data/db/migrate/20261013110400_add_break_minutes_to_hr_lite_settings.rb +6 -0
- data/db/migrate/20261013120000_grant_hr_lite_work_analytics.rb +14 -0
- data/db/migrate/20261013130000_add_office_location_to_hr_lite_holidays.rb +12 -0
- data/db/migrate/20261013130100_add_office_location_to_hr_lite_employee_profiles.rb +6 -0
- data/db/migrate/20261013140000_add_calendar_and_crm_to_hr_lite_work_mode_events.rb +9 -0
- data/db/migrate/20261013140100_add_custom_status_to_hr_lite_work_mode_events.rb +9 -0
- data/lib/hr_lite/configuration.rb +16 -1
- data/lib/hr_lite/notifications.rb +13 -4
- data/lib/hr_lite/permissions.rb +1 -0
- data/lib/hr_lite/role_seeds.rb +4 -2
- data/lib/hr_lite/seeds.rb +1 -1
- data/lib/hr_lite/version.rb +1 -1
- 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 %>">‹</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 %>">›</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,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,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,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
|
-
|
|
132
|
-
|
|
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
|
data/lib/hr_lite/permissions.rb
CHANGED
|
@@ -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" ],
|
data/lib/hr_lite/role_seeds.rb
CHANGED
|
@@ -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}"
|
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.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
|