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,42 @@
1
+ <% content_for(:page_title) { "Work time" } %>
2
+ <div class="hrl-page__head">
3
+ <h1 class="hrl-page__title">Work time<%= " — #{@names[@person.id]}" if @person %></h1>
4
+ <% if @person %>
5
+ <div class="hrl-page__actions">
6
+ <a class="hrl-btn" href="<%= hr_lite.admin_work_modes_path(date: @period.to, user_id: @person.id, anchor: "timeline") %>">Day timeline</a>
7
+ </div>
8
+ <% end %>
9
+ </div>
10
+
11
+ <section class="hrl-card">
12
+ <%= form_with url: hr_lite.admin_work_time_path, method: :get, local: true, class: "hrl-row" do %>
13
+ <input type="hidden" name="period" value="<%= @period.kind %>">
14
+ <input type="hidden" name="date" value="<%= @period.from %>">
15
+ <% if @period.kind == "custom" %><input type="hidden" name="from" value="<%= @period.from %>"><input type="hidden" name="to" value="<%= @period.to %>"><% end %>
16
+ <div class="hrl-field"><label for="wt-user">Employee</label>
17
+ <%= select_tag :user_id, options_for_select(@employees.map { |u| [ @names[u.id], u.id ] }, @person&.id), include_blank: "Everyone (totals only)", id: "wt-user", data: { hrl_filter: ("Type a name to filter" if @employees.size > 5) } %></div>
18
+ <% if @departments.any? %>
19
+ <div class="hrl-field"><label for="wt-dept">Department</label><%= select_tag :department, options_for_select(@departments, params[:department]), include_blank: "All", id: "wt-dept" %></div>
20
+ <% end %>
21
+ <div class="hrl-form-actions"><button type="submit" class="hrl-btn hrl-btn--primary">Show</button></div>
22
+ <% end %>
23
+ <p class="hrl-small hrl-muted"><%= @person ? "Opening one person's work time is recorded in the audit trail." : "Totals across #{pluralize(@employees.size, 'person')}; pick someone for their own breakdown." %></p>
24
+ </section>
25
+
26
+ <% if @period_error %><div class="hrl-errors" role="alert"><%= @period_error %></div><% end %>
27
+
28
+ <% keep = { user_id: @person&.id, department: params[:department] }.compact %>
29
+ <%= render "hr_lite/work_time/report", result: @result, period: @period, records: @records, keep: keep,
30
+ names: (@names if @person), timeline: @person.present?, path: ->(p) { hr_lite.admin_work_time_path(keep.merge(p)) } %>
31
+
32
+ <% if @result.gaps.any? %>
33
+ <section class="hrl-card">
34
+ <h2 class="hrl-card__title">Worth a look</h2>
35
+ <% @result.gaps.sort_by { |_, date, _| date }.first(200).each do |user_id, date, reason| %>
36
+ <div class="hrl-feed__item hrl-row hrl-small"><div class="hrl-feed__body"><strong><%= @names[user_id] %></strong> · <%= date.strftime("%a %-d %b") %> · <%= reason %></div>
37
+ <a class="hrl-btn" href="<%= hr_lite.admin_attendance_path(user_id, month: date.strftime("%Y-%m"), date: date, anchor: "regularize") %>">Open day</a></div>
38
+ <% end %>
39
+ </section>
40
+ <% end %>
41
+
42
+ <% content_for :scripts do %><%= javascript_include_tag "hr_lite/chart_tip", defer: true %><% end %>
@@ -0,0 +1,15 @@
1
+ <%# One-click link to the person's own calendar (config.calendar_connection). %>
2
+ <% link = HrLite.config.calendar_connection&.call(hr_current_user) %>
3
+ <% if link %>
4
+ <div class="hrl-row hrl-small">
5
+ <% if link[:connected] %>
6
+ <span class="hrl-badge hrl-badge--ok">Calendar connected</span>
7
+ <span class="hrl-muted"><%= link[:label] %> · meetings set your status by themselves</span>
8
+ <%= button_to "Disconnect", link[:disconnect_url], method: :delete, class: "hrl-btn",
9
+ form: { data: { turbo_confirm: "Stop reading your calendar?" } } %>
10
+ <% else %>
11
+ <a class="hrl-btn hrl-btn--block" href="<%= link[:connect_url] %>">Connect Google Calendar</a>
12
+ <span class="hrl-muted">One tap: meetings on your calendar then set your status to Meeting by themselves.</span>
13
+ <% end %>
14
+ </div>
15
+ <% end %>
@@ -0,0 +1,9 @@
1
+ <%# A lead from the CRM, searched as you type (config.crm_search). Optional. %>
2
+ <% if HrLite.config.crm_search %>
3
+ <div class="hrl-field" data-hrl-crm="<%= hr_lite.crm_search_work_mode_path %>">
4
+ <label for="<%= id %>-q">Link a lead (optional)</label>
5
+ <input type="search" class="hrl-input" id="<%= id %>-q" placeholder="Type a name or phone" autocomplete="off">
6
+ <label class="hrl-sr-only" for="<%= id %>">Lead</label>
7
+ <select name="crm_ref" id="<%= id %>"><option value="">No lead</option></select>
8
+ </div>
9
+ <% end %>
@@ -0,0 +1,26 @@
1
+ <%# The person's own status (text + emoji), Slack-style. Independent of check-in. %>
2
+ <% custom = status.custom
3
+ picks = %w[🏠 🏢 ✈️ 🚗 🚆 🏖️ 🤒 🤧 🩺 🍽️ ☕ 🎧 🎯 💻 📞 📅 🗓️ 🤝 💼 📍 🧳 🏔️ 🌴 🏕️ 📝 📊 🔒 🚫 ⏳ 🔕 🎉 🙏 👶 👨‍👩‍👧 🏥 📚 🎤 🛠️ 🌙 🌧️] %>
4
+ <% if custom %>
5
+ <div class="hrl-row hrl-row--mb hrl-small">
6
+ <span class="hrl-custom-status"><% if custom.emoji %><span aria-hidden="true"><%= custom.emoji %></span> <% end %><%= custom.title || "Status" %></span>
7
+ <% if custom.ends_at %><span class="hrl-muted">until <%= custom.ends_at.strftime(custom.ends_at.to_date == Date.current ? "%H:%M" : "%-d %b %H:%M") %></span><% end %>
8
+ <%= button_to "Clear status", hr_lite.clear_status_work_mode_path, method: :post, class: "hrl-btn" %>
9
+ </div>
10
+ <% end %>
11
+ <details>
12
+ <summary class="hrl-btn hrl-btn--block"><%= custom ? "Change my status" : "Set a status" %></summary>
13
+ <%= form_with url: hr_lite.status_work_mode_path, method: :post, local: true, data: { "hrl-emoji-form": true } do %>
14
+ <div class="hrl-field"><label for="st-emoji">Emoji</label>
15
+ <input class="hrl-input" id="st-emoji" name="emoji" maxlength="16" value="<%= custom&.emoji %>" placeholder="Pick or type one" autocomplete="off">
16
+ <div class="hrl-emoji-grid" role="group" aria-label="Pick an emoji">
17
+ <% picks.each do |e| %><button type="button" class="hrl-emoji" data-hrl-emoji="<%= e %>" aria-label="Use <%= e %>"><%= e %></button><% end %>
18
+ </div>
19
+ </div>
20
+ <div class="hrl-field"><label for="st-text">What's your status?</label>
21
+ <input class="hrl-input" id="st-text" name="text" maxlength="100" value="<%= custom&.title %>" placeholder="e.g. Visiting the Ladakh camp"></div>
22
+ <div class="hrl-field"><label for="st-clear">Clear after</label>
23
+ <%= select_tag :clear_after, options_for_select([ [ "30 minutes", "30" ], [ "1 hour", "60" ], [ "4 hours", "240" ], [ "Today", "today" ], [ "Don't clear", "never" ] ], "today"), id: "st-clear" %></div>
24
+ <button type="submit" class="hrl-btn hrl-btn--primary">Save status</button>
25
+ <% end %>
26
+ </details>
@@ -18,6 +18,9 @@
18
18
  <% if record&.check_out_at %>
19
19
  <span class="hrl-badge hrl-badge--muted">Out <%= record.check_out_at.strftime("%H:%M") %></span>
20
20
  <% end %>
21
+ <% if record&.work_mode_label %>
22
+ <span class="hrl-badge hrl-badge--ok"><%= record.work_mode_label %></span>
23
+ <% end %>
21
24
  <% if record&.flagged? %>
22
25
  <span class="hrl-badge hrl-badge--bad">Flagged</span>
23
26
  <% end %>
@@ -31,6 +34,9 @@
31
34
  <% if HrLite.config.attendance_slots %>
32
35
  <%= render "hr_lite/attendance/slot", record: record %>
33
36
  <% end %>
37
+ <% if HrLite.config.office_geofence %>
38
+ <%= render "hr_lite/attendance/work_mode", record: record %>
39
+ <% end %>
34
40
  <% if record&.flagged? %>
35
41
  <p class="hrl-small">Flagged: <%= record.flag_note %></p>
36
42
  <% end %>
@@ -50,6 +56,18 @@
50
56
  html: { data: { "hrl-geo-punch": true, "hrl-geo-required": HrLite.config.require_punch_location.presence } } do %>
51
57
  <input type="hidden" name="lat"><input type="hidden" name="lng">
52
58
  <input type="hidden" name="accuracy_m"><input type="hidden" name="geo_status">
59
+ <% if HrLite.config.office_geofence && HrLite::EmployeeProfile.where(user_id: hr_current_user.id).pick(:field_staff) %>
60
+ <details>
61
+ <summary class="hrl-btn hrl-btn--block">Checking in from the field?</summary>
62
+ <fieldset class="hrl-field">
63
+ <legend class="hrl-small">Where are you?</legend>
64
+ <label class="hrl-field--check"><input type="radio" name="field[mode]" value="travelling"> Travelling</label>
65
+ <label class="hrl-field--check"><input type="radio" name="field[mode]" value="outside_meeting"> Client meeting</label>
66
+ </fieldset>
67
+ <div class="hrl-field"><label for="field-purpose">Purpose</label><input type="text" class="hrl-input" id="field-purpose" name="field[purpose]" maxlength="120"></div>
68
+ <div class="hrl-field"><label for="field-place">Destination or client</label><input type="text" class="hrl-input" id="field-place" name="field[place]" maxlength="120"></div>
69
+ </details>
70
+ <% end %>
53
71
  <button type="submit" class="hrl-btn hrl-btn--primary hrl-btn--big hrl-btn--block">Check in</button>
54
72
  <p class="hrl-small hrl-muted" data-hrl-geo-status role="status" aria-live="polite"></p>
55
73
  <% end %>
@@ -74,6 +92,11 @@
74
92
  <% end %>
75
93
 
76
94
  <a class="hrl-card__link" href="<%= hr_lite.attendance_path %>">View my month</a>
95
+ <% if HrLite.config.office_geofence %>
96
+ · <a class="hrl-card__link" href="<%= hr_lite.wfh_requests_path %>">Work from home</a>
97
+ · <a class="hrl-card__link" href="<%= hr_lite.work_locations_path %>">WFH locations</a>
98
+ · <a class="hrl-card__link" href="<%= hr_lite.work_time_path %>">My work time</a>
99
+ <% end %>
77
100
 
78
101
  <% if local_assigns.key?(:attention) %>
79
102
  <div class="hrl-row">
@@ -0,0 +1,92 @@
1
+ <%# locals: record (today's AttendanceRecord or nil) %>
2
+ <% status = HrLite::WorkStatus.for(hr_current_user.id)
3
+ working = record&.check_in_at && !record.check_out_at
4
+ timed = status.overlay&.until || (status.primary.until if status.primary.until.respond_to?(:hour)) %>
5
+ <div class="hrl-row hrl-row--mb hrl-small" role="status">
6
+ <span class="hrl-modes"><%= hrl_status_icons(status) %></span>
7
+ <span><strong><%= status.primary.label %></strong><% if status.overlay %> · <%= status.overlay.label %><% end %></span>
8
+ <% if status.primary.source == "system" && !status.overlay %><span class="hrl-muted">(automatic)</span><% end %>
9
+ <% if timed %>
10
+ <span class="hrl-muted">until <%= timed.strftime("%H:%M") %> ·
11
+ <span data-hrl-countdown="<%= timed.iso8601 %>"><%= hrl_duration([ timed - Time.current, 0 ].max) %> left</span></span>
12
+ <% end %>
13
+ </div>
14
+ <%= render "hr_lite/attendance/custom_status", status: status %>
15
+ <%= render "hr_lite/attendance/calendar_connection" %>
16
+ <% if status.half_day_leave %><p class="hrl-small hrl-muted">Half-day leave today (<%= status.half_day_leave.half_day_part || "half day" %>).</p><% end %>
17
+
18
+ <% if working %>
19
+ <% if status.overlay %>
20
+ <%= button_to "End #{status.overlay.label.downcase} now", hr_lite.end_overlay_work_mode_path, method: :post, class: "hrl-btn hrl-btn--block" %>
21
+ <% end %>
22
+ <details>
23
+ <summary class="hrl-btn hrl-btn--block">Change my mode</summary>
24
+ <div class="hrl-mode-switch">
25
+ <% %w[lunch short_break office].each do |mode| %>
26
+ <%= button_to hr_lite.work_mode_path, params: { mode: mode }, method: :post, class: "hrl-btn hrl-btn--block" do %>
27
+ <%= hrl_mode_icon(HrLite::WorkStatus::Entry.new(mode: mode, label: HrLite::WorkModeEvent.label(mode))) %>
28
+ <%= HrLite::WorkModeEvent.label(mode) %>
29
+ <% end %>
30
+ <% end %>
31
+ <% if HrLite::WfhRequest.approved_for(hr_current_user.id, Date.current) || HrLite::EmployeeProfile.where(user_id: hr_current_user.id).pick(:permanent_wfh) %>
32
+ <%= button_to "Work from home", hr_lite.work_mode_path, params: { mode: "wfh" }, method: :post, class: "hrl-btn hrl-btn--block" %>
33
+ <% end %>
34
+
35
+ <details>
36
+ <summary class="hrl-btn hrl-btn--block">Busy / focus</summary>
37
+ <%= form_with url: hr_lite.work_mode_path, method: :post, local: true do %>
38
+ <input type="hidden" name="mode" value="busy">
39
+ <div class="hrl-field"><label for="busy-minutes">For</label>
40
+ <%= select_tag :minutes, options_for_select([ [ "30 minutes", 30 ], [ "1 hour", 60 ], [ "2 hours", 120 ] ]), id: "busy-minutes" %></div>
41
+ <button type="submit" class="hrl-btn hrl-btn--primary">Set busy</button>
42
+ <% end %>
43
+ </details>
44
+ <details>
45
+ <summary class="hrl-btn hrl-btn--block">Meeting</summary>
46
+ <%= form_with url: hr_lite.work_mode_path, method: :post, local: true do %>
47
+ <input type="hidden" name="mode" value="meeting">
48
+ <div class="hrl-field"><label for="mt-title">Meeting</label><input class="hrl-input" id="mt-title" name="title" maxlength="120" required></div>
49
+ <div class="hrl-field"><label for="mt-client">Client (optional)</label><input class="hrl-input" id="mt-client" name="client" maxlength="120"></div>
50
+ <%= render "hr_lite/attendance/crm_picker", id: "mt-crm" %>
51
+ <div class="hrl-field"><label for="mt-until">Ends at</label><input class="hrl-input" type="time" id="mt-until" name="until" required></div>
52
+ <button type="submit" class="hrl-btn hrl-btn--primary">Start meeting</button>
53
+ <% end %>
54
+ </details>
55
+ <details>
56
+ <summary class="hrl-btn hrl-btn--block">Travelling</summary>
57
+ <%= form_with url: hr_lite.work_mode_path, method: :post, local: true, data: { turbo_confirm: "Switch to Travelling?" } do %>
58
+ <input type="hidden" name="mode" value="travelling">
59
+ <div class="hrl-field"><label for="tr-dest">Going to</label><input class="hrl-input" id="tr-dest" name="destination" maxlength="120" required></div>
60
+ <div class="hrl-field"><label for="tr-why">Purpose</label><input class="hrl-input" id="tr-why" name="title" maxlength="120" required></div>
61
+ <div class="hrl-field"><label for="tr-client">Client (optional)</label><input class="hrl-input" id="tr-client" name="client" maxlength="120"></div>
62
+ <%= render "hr_lite/attendance/crm_picker", id: "tr-crm" %>
63
+ <div class="hrl-field"><label for="tr-until">Back by (optional)</label><input class="hrl-input" type="time" id="tr-until" name="until"></div>
64
+ <button type="submit" class="hrl-btn hrl-btn--primary">Start travelling</button>
65
+ <% end %>
66
+ </details>
67
+ <details>
68
+ <summary class="hrl-btn hrl-btn--block">Client meeting (outside)</summary>
69
+ <%= form_with url: hr_lite.work_mode_path, method: :post, local: true, data: { turbo_confirm: "Switch to an outside-office meeting?" } do %>
70
+ <input type="hidden" name="mode" value="outside_meeting">
71
+ <%= render "hr_lite/attendance/crm_picker", id: "om-crm" %>
72
+ <div class="hrl-field"><label for="om-client">Client or prospect<%= " (or link a lead above)" if HrLite.config.crm_search %></label><input class="hrl-input" id="om-client" name="client" maxlength="120"<%= " required" unless HrLite.config.crm_search %>></div>
73
+ <div class="hrl-field"><label for="om-why">Purpose</label><input class="hrl-input" id="om-why" name="title" maxlength="120" required></div>
74
+ <div class="hrl-field"><label for="om-where">Where (optional)</label><input class="hrl-input" id="om-where" name="destination" maxlength="120"></div>
75
+ <div class="hrl-field"><label for="om-until">Ends at (optional)</label><input class="hrl-input" type="time" id="om-until" name="until"></div>
76
+ <button type="submit" class="hrl-btn hrl-btn--primary">Start client meeting</button>
77
+ <% end %>
78
+ </details>
79
+ <details>
80
+ <summary class="hrl-btn hrl-btn--block">Out of office</summary>
81
+ <%= form_with url: hr_lite.work_mode_path, method: :post, local: true do %>
82
+ <input type="hidden" name="mode" value="out_of_office">
83
+ <div class="hrl-field"><label for="oo-why">Reason</label>
84
+ <%= select_tag :category, options_for_select(HrLite::WorkModeSwitch::OUT_OF_OFFICE_REASONS.invert), id: "oo-why" %></div>
85
+ <div class="hrl-field"><label for="oo-until">Back by</label><input class="hrl-input" type="time" id="oo-until" name="until" required></div>
86
+ <p class="hrl-small hrl-muted">Away for a full day? <a href="<%= hr_lite.new_leave_request_path %>">Apply for leave</a> instead.</p>
87
+ <button type="submit" class="hrl-btn hrl-btn--primary">Set out of office</button>
88
+ <% end %>
89
+ </details>
90
+ </div>
91
+ </details>
92
+ <% end %>
@@ -43,7 +43,7 @@
43
43
  <dt>Worked</dt><dd><%= hrl_duration(rec&.worked_duration) %></dd>
44
44
  <% if part %><dt>Half</dt><dd><%= HrLite::DAY_PARTS.rassoc(part)&.first %></dd><% end %>
45
45
  <% if leave %><dt>Leave</dt><dd><%= leave.leave_type.name %></dd><% end %>
46
- <% if @day_info.kind == :holiday %><dt>Holiday</dt><dd><%= HrLite::Holiday.find_by(date: @day)&.name %></dd><% end %>
46
+ <% if @day_info.kind == :holiday %><dt>Holiday</dt><dd><%= HrLite::Holiday.company_wide.for_office(HrLite::WorkingCalendar.office_for(hr_current_user.id)).where(date: @day).pick(:name) %></dd><% end %>
47
47
  <% if rec&.flagged? %><dt>Flag</dt><dd><%= rec.flag_note %></dd><% end %>
48
48
  <% if @day_ticket %><dt>Ticket</dt><dd><%= hrl_request_status_badge(@day_ticket.status) %> <%= @day_ticket.times_label %></dd><% end %>
49
49
  </dl>
@@ -55,4 +55,7 @@
55
55
 
56
56
  <% content_for :scripts do %>
57
57
  <%= javascript_include_tag "hr_lite/geo_punch", defer: true %>
58
+ <%= javascript_include_tag "hr_lite/countdown", defer: true %>
59
+ <%= javascript_include_tag "hr_lite/crm_picker", defer: true %>
60
+ <%= javascript_include_tag "hr_lite/emoji_pick", defer: true %>
58
61
  <% end %>
@@ -1,6 +1,6 @@
1
1
  <section class="hrl-card">
2
2
  <h2 class="hrl-card__title">Upcoming holidays</h2>
3
- <% holidays = HrLite::Holiday.company_wide.upcoming.where(date: ..Date.current + 60).limit(3).to_a %>
3
+ <% holidays = HrLite::Holiday.company_wide.for_office(HrLite::WorkingCalendar.office_for(hr_current_user.id)).upcoming.where(date: ..Date.current + 60).limit(3).to_a %>
4
4
  <% if holidays.any? %>
5
5
  <div class="hrl-feed">
6
6
  <% holidays.each do |holiday| %>
@@ -0,0 +1,31 @@
1
+ <%# Today's check-ins: on time, late, not in yet. Hover or tap a person for times. %>
2
+ <% groups = HrLite::TodayCheckins.new.groups
3
+ tip = ->(p) do
4
+ r = p.record
5
+ parts = [ hr_display_name(p.user) ]
6
+ if r
7
+ parts << "In #{r.check_in_at.strftime('%H:%M')}#{" (#{p.late_minutes} min late)" if p.late?}"
8
+ parts << (r.check_out_at ? "Out #{r.check_out_at.strftime('%H:%M')} · #{hrl_duration(r.worked_duration)} worked" : "#{hrl_duration(Time.current - r.check_in_at)} so far")
9
+ else
10
+ parts << "Not checked in yet"
11
+ end
12
+ parts.join(" · ")
13
+ end %>
14
+ <section class="hrl-card">
15
+ <h2 class="hrl-card__title">Today's check-ins</h2>
16
+ <% [ [ :on_time, "On time", "ok" ], [ :late, "Late", "warn" ], [ :not_in, "Not in yet", "muted" ] ].each do |key, label, tone| %>
17
+ <div class="hrl-checkins">
18
+ <p class="hrl-small"><span class="hrl-badge hrl-badge--<%= tone %>"><%= label %> · <%= groups[key].size %></span></p>
19
+ <% if groups[key].any? %>
20
+ <ul class="hrl-checkins__people">
21
+ <% groups[key].each do |p| %>
22
+ <li><button type="button" class="hrl-person hrl-person--<%= tone %>" data-hrl-tip="<%= tip.(p) %>" title="<%= tip.(p) %>" aria-label="<%= tip.(p) %>">
23
+ <%= hr_display_name(p.user).to_s.scan(/[[:alpha:]]+/).first(2).map { |w| w[0] }.join.upcase %>
24
+ </button></li>
25
+ <% end %>
26
+ </ul>
27
+ <% end %>
28
+ </div>
29
+ <% end %>
30
+ <a class="hrl-card__link" href="<%= hr_lite.team_path %>">Team board</a>
31
+ </section>
@@ -30,6 +30,7 @@
30
30
  <% end %>
31
31
  </section>
32
32
  <% end %>
33
+ <%= render "hr_lite/home/today_checkins" %>
33
34
  <%= render "hr_lite/holidays/next_holiday" %>
34
35
 
35
36
  <section class="hrl-card">
@@ -72,4 +73,8 @@
72
73
 
73
74
  <% content_for :scripts do %>
74
75
  <%= javascript_include_tag "hr_lite/geo_punch", defer: true %>
76
+ <%= javascript_include_tag "hr_lite/chart_tip", defer: true %>
77
+ <%= javascript_include_tag "hr_lite/countdown", defer: true %>
78
+ <%= javascript_include_tag "hr_lite/crm_picker", defer: true %>
79
+ <%= javascript_include_tag "hr_lite/emoji_pick", defer: true %>
75
80
  <% end %>
@@ -20,19 +20,40 @@
20
20
  <div class="hrl-kpi"><div class="hrl-kpi__value"><%= @board.rows.size %></div><div class="hrl-kpi__label">Team size</div></div>
21
21
  </div>
22
22
 
23
+ <% if @statuses %>
24
+ <section class="hrl-card">
25
+ <%= form_with url: hr_lite.team_path, method: :get, local: true, class: "hrl-row" do %>
26
+ <% if @departments.any? %>
27
+ <div class="hrl-field"><label for="team-dept">Department</label><%= select_tag :department, options_for_select(@departments, params[:department]), include_blank: "All", id: "team-dept" %></div>
28
+ <% end %>
29
+ <div class="hrl-field"><label for="team-mode">Mode</label><%= select_tag :mode, options_for_select(HrLite::WorkModeEvent::MODES.map { |k, v| [ v[0], k ] } + HrLite::WorkStatus::DERIVED.map { |k, v| [ v, k ] }.reject { |_, k| k == "offline" }, @mode), include_blank: "All", id: "team-mode" %></div>
30
+ <div class="hrl-field"><label for="team-avail">Availability</label><%= select_tag :availability, options_for_select([ [ "Available", "available" ], [ "Not available", "unavailable" ] ], @availability), include_blank: "All", id: "team-avail" %></div>
31
+ <div class="hrl-form-actions"><button type="submit" class="hrl-btn">Filter</button></div>
32
+ <% end %>
33
+ </section>
34
+ <% end %>
35
+
23
36
  <section class="hrl-card">
24
- <% if @board.rows.any? %>
37
+ <% rows = @rows || @board.rows %>
38
+ <% if rows.any? %>
25
39
  <div class="hrl-table-wrap">
26
40
  <table class="hrl-table hrl-table--stack">
27
- <thead><tr><th>Name</th><th>Status</th><th>Hours <%= @date == Date.current ? "today" : "that day" %></th><th>Hours this month</th></tr></thead>
41
+ <thead><tr><th>Name</th><% if @statuses %><th>Now</th><% end %><th>Status</th><th>Hours <%= @date == Date.current ? "today" : "that day" %></th><th>Hours this month</th></tr></thead>
28
42
  <tbody>
29
- <% @board.rows.each do |row| %>
43
+ <% rows.each do |row| %>
30
44
  <tr>
31
45
  <td data-label="Name">
32
46
  <span><%= tag.strong hr_display_name(row.user), title: ("On leave: #{row.leave.date_range_label}" if row.leave) %><% if row.profile&.designation.present? %>
33
47
  <span class="hrl-small hrl-muted"> · <%= row.profile.designation %></span><% end %><% if HrLite.config.attendance_slots && row.profile&.buffer_minutes %>
34
48
  <span class="hrl-badge hrl-badge--muted">+<%= row.profile.buffer_minutes / 60 %>h buffer · <%= HrLite::EmployeeProfile::BUFFER_REASONS[row.profile.buffer_reason] %></span><% end %></span>
35
49
  </td>
50
+ <% if (st = @statuses&.dig(row.user.id)) %>
51
+ <td data-label="Now"><span class="hrl-modes"><%= hrl_status_icons(st) %></span>
52
+ <span class="hrl-small"><%= st.primary.place || st.primary.label %><% if st.overlay %> · <%= st.overlay.label %><% end %>
53
+ <% if (till = st.overlay&.until || (st.primary.until if st.primary.until.respond_to?(:hour))) %> · until <%= till.strftime("%H:%M") %><% end %></span>
54
+ <% if st.updated_at %><span class="hrl-small hrl-muted"> · since <%= st.updated_at.strftime("%H:%M") %></span><% end %>
55
+ <% if st.custom %><div class="hrl-custom-status hrl-small"><% if st.custom.emoji %><span aria-hidden="true"><%= st.custom.emoji %></span> <% end %><%= st.custom.title %></div><% end %></td>
56
+ <% end %>
36
57
  <td data-label="Status"><%= hrl_team_status(row) %></td>
37
58
  <td data-label="Hours <%= @date == Date.current ? "today" : "that day" %>" class="hrl-num"><%= hrl_duration(row.day_seconds) %></td>
38
59
  <td data-label="Hours this month" class="hrl-num"><%= hrl_duration(row.month_seconds) %></td>
@@ -42,7 +63,7 @@
42
63
  </table>
43
64
  </div>
44
65
  <% else %>
45
- <p class="hrl-card--empty">Nobody to show for this date.</p>
66
+ <p class="hrl-card--empty">Nobody to show<%= @rows ? " for these filters" : " for this date" %>.</p>
46
67
  <% end %>
47
68
  <p class="hrl-small hrl-muted hrl-mt">Forgot a punch yourself?
48
69
  <a class="hrl-card__link" href="<%= hr_lite.new_regularization_request_path %>">Raise a regularization ticket</a></p>
@@ -0,0 +1,70 @@
1
+ <% content_for(:page_title) { "Work from home" } %>
2
+ <% content_for(:back, "Attendance") %>
3
+ <div class="hrl-page__head">
4
+ <h1 class="hrl-page__title">Work from home</h1>
5
+ </div>
6
+
7
+ <section class="hrl-card">
8
+ <h2 class="hrl-card__title">Request WFH</h2>
9
+ <p class="hrl-small hrl-muted">Pick dates from today up to <%= HrLite::WfhRequest::MAX_DAYS_AHEAD %> days ahead. HR approves or rejects; only approved dates let you check in away from the office.</p>
10
+ <%= form_with model: @request, url: hr_lite.wfh_requests_path, scope: :wfh_request, local: true do |f| %>
11
+ <%= render "hr_lite/shared/form_errors", record: @request %>
12
+ <% min, max = @window.first.iso8601, @window.last.iso8601 %>
13
+ <div class="hrl-field"><%= f.label :start_date, "From" %><%= f.date_field :start_date, min: min, max: max, required: true %></div>
14
+ <div class="hrl-field"><%= f.label :end_date, "To (same day for one day)" %><%= f.date_field :end_date, min: min, max: max, required: true %></div>
15
+ <div class="hrl-field"><%= f.label :reason, "Why do you need WFH?" %><%= f.text_area :reason, rows: 3, required: true %></div>
16
+ <div class="hrl-form-actions"><%= f.submit "Send to HR", class: "hrl-btn hrl-btn--primary" %></div>
17
+ <% end %>
18
+ </section>
19
+
20
+ <section class="hrl-card">
21
+ <h2 class="hrl-card__title">Next <%= HrLite::WfhRequest::MAX_DAYS_AHEAD %> days</h2>
22
+ <% marks = { "approved" => [ "WFH", "Approved", "present" ], "pending" => [ "Pend", "Pending", "pending" ], "rejected" => [ "Rej", "Rejected", "absent" ] } %>
23
+ <div class="hrl-mgrid" role="group" aria-label="Your WFH dates">
24
+ <% %w[Mo Tu We Th Fr Sa Su].each do |d| %><div class="hrl-mgrid__head"><%= d %></div><% end %>
25
+ <% (@window.first.cwday - 1).times do %><div></div><% end %>
26
+ <% @window.each do |date| %>
27
+ <% code, word, mod = marks[@day_status[date]] %>
28
+ <div class="<%= class_names("hrl-mgrid__day", "hrl-mgrid__day--#{mod}": mod, "hrl-mgrid__day--today": date == Date.current) %>"
29
+ role="group" aria-label="<%= date.strftime('%-d %b') %><%= ", #{word}" if word %>">
30
+ <span class="hrl-mgrid__num"><%= date.day %></span>
31
+ <% if code %><span class="hrl-mgrid__tag"><%= code %></span><% end %>
32
+ </div>
33
+ <% end %>
34
+ </div>
35
+ <div class="hrl-legend hrl-small">
36
+ <% marks.each_value do |_code, word, mod| %>
37
+ <span class="hrl-badge hrl-mgrid__key hrl-mgrid__day--<%= mod %>"><%= word %></span>
38
+ <% end %>
39
+ </div>
40
+ </section>
41
+
42
+ <section class="hrl-card">
43
+ <h2 class="hrl-card__title">My requests</h2>
44
+ <% if @requests.any? %>
45
+ <div class="hrl-table-wrap">
46
+ <table class="hrl-table hrl-table--stack">
47
+ <thead><tr><th>Dates</th><th>Reason</th><th>Status</th><th>HR remarks</th><th></th></tr></thead>
48
+ <tbody>
49
+ <% @requests.each do |request| %>
50
+ <tr>
51
+ <td data-label="Dates"><%= request.dates_label %> <span class="hrl-small hrl-muted">· <%= pluralize(request.days, "day") %></span></td>
52
+ <td data-label="Reason"><%= request.reason %></td>
53
+ <td data-label="Status"><%= hrl_request_status_badge(request.status) %></td>
54
+ <td data-label="HR remarks" class="hrl-small hrl-muted"><%= request.decision_note.presence || "—" %></td>
55
+ <td data-label="" class="<%= 'hrl-cell--void' unless request.pending? %>">
56
+ <% if request.pending? %>
57
+ <%= button_to "Cancel", hr_lite.cancel_wfh_request_path(request), method: :post,
58
+ class: "hrl-btn hrl-btn--danger", form: { data: { turbo_confirm: "Cancel this request?" } } %>
59
+ <% end %>
60
+ </td>
61
+ </tr>
62
+ <% end %>
63
+ </tbody>
64
+ </table>
65
+ </div>
66
+ <%= hrl_pagination %>
67
+ <% else %>
68
+ <p class="hrl-card--empty">No WFH requests yet.</p>
69
+ <% end %>
70
+ </section>
@@ -0,0 +1,52 @@
1
+ <% content_for(:page_title) { "WFH locations" } %>
2
+ <% content_for(:back, "Attendance") %>
3
+ <div class="hrl-page__head">
4
+ <h1 class="hrl-page__title">My WFH locations</h1>
5
+ </div>
6
+
7
+ <section class="hrl-card">
8
+ <p class="hrl-small hrl-muted">On an approved WFH day you check in within <%= HrLite::WorkLocation::FENCE_M %> m of your active location.
9
+ Register up to <%= HrLite::WorkLocation::MAX_LIVE %> places; HR approves each one. Your location is read only when you check in or tap
10
+ "Register here" — never tracked in between. Teammates see "Home", never the address.</p>
11
+ <% if @locations.any? %>
12
+ <% @locations.each do |location| %>
13
+ <div class="hrl-feed__item hrl-row">
14
+ <div class="hrl-feed__body">
15
+ <strong><%= location.name %></strong>
16
+ <% if location.active? %><span class="hrl-badge hrl-badge--ok">Active</span><% end %>
17
+ <%= hrl_request_status_badge(location.status) %>
18
+ <% if location.address.present? %><div class="hrl-small hrl-muted"><%= location.address %></div><% end %>
19
+ <% if location.decision_note.present? %><div class="hrl-small hrl-muted">HR: <%= location.decision_note %></div><% end %>
20
+ </div>
21
+ <% if location.approved? && !location.active? %>
22
+ <%= button_to "Use this", hr_lite.activate_work_location_path(location), method: :post, class: "hrl-btn" %>
23
+ <% end %>
24
+ <%= button_to "Remove", hr_lite.remove_work_location_path(location), method: :post, class: "hrl-btn hrl-btn--danger",
25
+ form: { data: { turbo_confirm: "Remove #{location.name}?" } } %>
26
+ </div>
27
+ <% end %>
28
+ <% else %>
29
+ <p class="hrl-card--empty">No locations yet.</p>
30
+ <% end %>
31
+ </section>
32
+
33
+ <%= render "hr_lite/shared/form_errors", record: @location %>
34
+
35
+ <% if @locations.size < HrLite::WorkLocation::MAX_LIVE %>
36
+ <section class="hrl-card">
37
+ <h2 class="hrl-card__title">Register where you are now</h2>
38
+ <%= form_with url: hr_lite.work_locations_path, method: :post, local: true,
39
+ html: { data: { "hrl-geo-punch": true, "hrl-geo-required": true } } do %>
40
+ <div class="hrl-field"><label for="loc-name">Name</label><%= text_field_tag :name, @location.name, required: true, maxlength: 60, placeholder: "Home", id: "loc-name" %></div>
41
+ <div class="hrl-field"><label for="loc-address">Address or landmark (optional, HR only)</label><%= text_field_tag :address, @location.address, maxlength: 200, id: "loc-address" %></div>
42
+ <input type="hidden" name="lat"><input type="hidden" name="lng">
43
+ <input type="hidden" name="accuracy_m"><input type="hidden" name="geo_status">
44
+ <button type="submit" class="hrl-btn hrl-btn--primary hrl-btn--block">Register here</button>
45
+ <p class="hrl-small hrl-muted" data-hrl-geo-status role="status" aria-live="polite"></p>
46
+ <% end %>
47
+ </section>
48
+ <% end %>
49
+
50
+ <% content_for :scripts do %>
51
+ <%= javascript_include_tag "hr_lite/geo_punch", defer: true %>
52
+ <% end %>
@@ -0,0 +1,44 @@
1
+ <%# Stacked bars per bucket (day / week / month), Offline left out. %>
2
+ <% buckets = result.by_bucket
3
+ totals = buckets.transform_values { |m| m.except("offline").values.sum }
4
+ max = [ totals.values.max.to_f, 1 ].max
5
+ w = 12; gap = 4; h = 100
6
+ fmt = { day: "%-d", week: "%-d %b", month: "%b" }[period.bucket] %>
7
+ <section class="hrl-card">
8
+ <h2 class="hrl-card__title">By <%= period.bucket %></h2>
9
+ <div class="hrl-chart-scroll">
10
+ <svg class="hrl-bars" viewBox="0 0 <%= buckets.size * (w + gap) %> <%= h + 14 %>" role="img"
11
+ aria-label="Time per <%= period.bucket %> stacked by work mode; tallest bar <%= hrl_duration(max) %>; details in the table">
12
+ <% buckets.each_with_index do |(key, modes), i| %>
13
+ <% x = i * (w + gap); y = h %>
14
+ <a href="<%= path.(period: period.bucket == :day ? "day" : (period.bucket == :week ? "week" : "month"), date: key) %>">
15
+ <% modes.except("offline").sort.each do |mode, secs| %>
16
+ <% next unless secs.positive? %>
17
+ <% bh = secs / max * h; y -= bh %>
18
+ <rect x="<%= x %>" y="<%= y.round(2) %>" width="<%= w %>" height="<%= bh.round(2) %>" class="hrl-fill--<%= mode %>">
19
+ <title><%= key.strftime(fmt) %> · <%= label.(mode) %>: <%= hrl_duration(secs) %></title>
20
+ </rect>
21
+ <% end %>
22
+ <rect x="<%= x %>" y="0" width="<%= w %>" height="<%= h %>" fill="transparent"><title><%= key.strftime(fmt) %>: <%= hrl_duration(totals[key]) %> — open</title></rect>
23
+ </a>
24
+ <% if buckets.size <= 16 || (i % 7).zero? %>
25
+ <text x="<%= x + w / 2.0 %>" y="<%= h + 11 %>" text-anchor="middle" class="hrl-bars__label"><%= key.strftime(fmt) %></text>
26
+ <% end %>
27
+ <% end %>
28
+ </svg>
29
+ </div>
30
+ <details class="hrl-small">
31
+ <summary>Show as a table</summary>
32
+ <table class="hrl-table">
33
+ <thead><tr><th><%= period.bucket.to_s.capitalize %></th><th class="hrl-num">Working</th><th class="hrl-num">Breaks</th><th class="hrl-num">Total</th></tr></thead>
34
+ <tbody>
35
+ <% buckets.each do |key, modes| %>
36
+ <tr><td><%= key.strftime(period.bucket == :day ? "%a %-d %b" : fmt) %></td>
37
+ <td class="hrl-num"><%= hrl_duration(HrLite::WorkModeDurations::WORKING.sum { |m| modes[m] }) %></td>
38
+ <td class="hrl-num"><%= hrl_duration(HrLite::WorkModeDurations::BREAKS.sum { |m| modes[m] }) %></td>
39
+ <td class="hrl-num"><%= hrl_duration(totals[key]) %></td></tr>
40
+ <% end %>
41
+ </tbody>
42
+ </table>
43
+ </details>
44
+ </section>
@@ -0,0 +1,36 @@
1
+ <%# Share of tracked time (working + breaks + out of office; Offline excluded). %>
2
+ <% total = result.tracked
3
+ modes = result.by_mode.except("offline").select { |_, s| s.positive? }.sort_by { |_, s| -s }
4
+ counts = result.segments.group_by(&:mode).transform_values(&:size)
5
+ offset = 25.0 %>
6
+ <section class="hrl-card">
7
+ <h2 class="hrl-card__title">Where the time went</h2>
8
+ <% if total.positive? %>
9
+ <div class="hrl-chart-row">
10
+ <svg class="hrl-donut" viewBox="0 0 42 42" role="img" aria-label="Share of tracked time by work mode; details in the table">
11
+ <circle cx="21" cy="21" r="15.915" fill="none" class="hrl-donut__track" stroke-width="6"></circle>
12
+ <% modes.each do |mode, secs| %>
13
+ <% pct = secs * 100.0 / total %>
14
+ <circle cx="21" cy="21" r="15.915" fill="none" stroke-width="6" class="hrl-stroke--<%= mode %>" data-hrl-tip tabindex="0"
15
+ stroke-dasharray="<%= pct.round(3) %> <%= (100 - pct).round(3) %>" stroke-dashoffset="<%= offset.round(3) %>">
16
+ <title><%= label.(mode) %>: <%= hrl_duration(secs) %> (<%= pct.round %>%)</title>
17
+ </circle>
18
+ <% offset -= pct %>
19
+ <% end %>
20
+ </svg>
21
+ <table class="hrl-table hrl-small">
22
+ <thead><tr><th>Mode</th><th class="hrl-num">Time</th><th class="hrl-num">Share</th><th class="hrl-num">Stretches</th></tr></thead>
23
+ <tbody>
24
+ <% modes.each do |mode, secs| %>
25
+ <tr><td><span class="hrl-swatch hrl-fill-bg--<%= mode %>" aria-hidden="true"></span> <%= label.(mode) %></td>
26
+ <td class="hrl-num"><%= hrl_duration(secs) %></td><td class="hrl-num"><%= (secs * 100.0 / total).round %>%</td>
27
+ <td class="hrl-num"><%= counts[mode] %></td></tr>
28
+ <% end %>
29
+ </tbody>
30
+ </table>
31
+ </div>
32
+ <p class="hrl-small hrl-muted">Shares are of tracked time (<%= hrl_duration(total) %>: working, breaks and out of office). Offline (<%= hrl_duration(result.offline) %>) is not included.</p>
33
+ <% else %>
34
+ <p class="hrl-card--empty">Only offline time recorded.</p>
35
+ <% end %>
36
+ </section>