hr_lite 0.2.1 → 0.4.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 +96 -0
- data/README.md +2 -1
- data/app/assets/stylesheets/hr_lite/hr_lite.css +35 -4
- data/app/controllers/hr_lite/admin/comp_off_requests_controller.rb +38 -0
- data/app/controllers/hr_lite/admin/employees_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/leave_balances_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/leave_types_controller.rb +1 -1
- data/app/controllers/hr_lite/admin/regularization_requests_controller.rb +38 -0
- data/app/controllers/hr_lite/comp_off_requests_controller.rb +46 -0
- data/app/controllers/hr_lite/leave_balances_controller.rb +1 -1
- data/app/controllers/hr_lite/leave_requests_controller.rb +1 -1
- data/app/controllers/hr_lite/org_controller.rb +51 -0
- data/app/controllers/hr_lite/regularization_requests_controller.rb +36 -0
- data/app/controllers/hr_lite/team_controller.rb +11 -0
- data/app/helpers/hr_lite/application_helper.rb +62 -3
- data/app/jobs/hr_lite/leave_year_rollover_job.rb +18 -6
- data/app/models/hr_lite/comp_off_request.rb +177 -0
- data/app/models/hr_lite/employee_profile.rb +55 -0
- data/app/models/hr_lite/leave_balance.rb +43 -10
- data/app/models/hr_lite/leave_request.rb +23 -5
- data/app/models/hr_lite/leave_type.rb +17 -0
- data/app/models/hr_lite/regularization_request.rb +181 -0
- data/app/services/hr_lite/team_day.rb +84 -0
- data/app/views/hr_lite/admin/comp_off_requests/index.html.erb +37 -0
- data/app/views/hr_lite/admin/comp_off_requests/show.html.erb +47 -0
- data/app/views/hr_lite/admin/employees/_form.html.erb +7 -0
- data/app/views/hr_lite/admin/employees/show.html.erb +1 -0
- data/app/views/hr_lite/admin/leave_balances/index.html.erb +1 -1
- data/app/views/hr_lite/admin/leave_requests/index.html.erb +2 -0
- data/app/views/hr_lite/admin/leave_types/_form.html.erb +4 -0
- data/app/views/hr_lite/admin/regularization_requests/index.html.erb +37 -0
- data/app/views/hr_lite/admin/regularization_requests/show.html.erb +50 -0
- data/app/views/hr_lite/admin/shared/_approvals_tabs.html.erb +11 -0
- data/app/views/hr_lite/attendance/show.html.erb +3 -0
- data/app/views/hr_lite/comp_off_requests/index.html.erb +42 -0
- data/app/views/hr_lite/comp_off_requests/new.html.erb +28 -0
- data/app/views/hr_lite/employee_profiles/show.html.erb +3 -0
- data/app/views/hr_lite/leave_balances/index.html.erb +1 -1
- data/app/views/hr_lite/leave_requests/_balance_chips.html.erb +2 -2
- data/app/views/hr_lite/leave_requests/index.html.erb +1 -0
- data/app/views/hr_lite/org/_node.html.erb +14 -0
- data/app/views/hr_lite/org/show.html.erb +38 -0
- data/app/views/hr_lite/regularization_requests/index.html.erb +41 -0
- data/app/views/hr_lite/regularization_requests/new.html.erb +30 -0
- data/app/views/hr_lite/team/show.html.erb +48 -0
- data/app/views/layouts/hr_lite/application.html.erb +1 -1
- data/config/routes.rb +14 -0
- data/db/migrate/20260719104315_add_comp_off_to_hr_lite_leave_types.rb +6 -0
- data/db/migrate/20260719104316_create_hr_lite_comp_off_requests.rb +24 -0
- data/db/migrate/20260719104317_create_hr_lite_regularization_requests.rb +20 -0
- data/db/migrate/20260719162154_add_manager_to_hr_lite_employee_profiles.rb +8 -0
- data/lib/hr_lite/configuration.rb +14 -0
- data/lib/hr_lite/leave_year.rb +33 -0
- data/lib/hr_lite/notifications.rb +14 -2
- data/lib/hr_lite/seeds.rb +16 -1
- data/lib/hr_lite/version.rb +1 -1
- data/lib/hr_lite.rb +8 -0
- metadata +27 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Comp-off approvals" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Comp-off approvals</h1>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<%= render "hr_lite/admin/shared/approvals_tabs", active: :comp_off %>
|
|
7
|
+
|
|
8
|
+
<div class="hrl-row" style="margin-bottom:.85rem;">
|
|
9
|
+
<span class="hrl-small hrl-muted">Status</span>
|
|
10
|
+
<% HrLite::CompOffRequest::STATUSES.each do |status| %>
|
|
11
|
+
<a class="hrl-btn <%= 'hrl-btn--primary' if @status == status %>"
|
|
12
|
+
href="<%= hr_lite.admin_comp_off_requests_path(status: status) %>"><%= status.humanize %></a>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<section class="hrl-card">
|
|
17
|
+
<% if @requests.any? %>
|
|
18
|
+
<div class="hrl-table-wrap">
|
|
19
|
+
<table class="hrl-table hrl-table--stack">
|
|
20
|
+
<thead><tr><th>Employee</th><th>Day worked</th><th>Credit</th><th></th></tr></thead>
|
|
21
|
+
<tbody>
|
|
22
|
+
<% @requests.each do |request| %>
|
|
23
|
+
<tr>
|
|
24
|
+
<td data-label="Employee"><%= hr_display_name(request.user) %></td>
|
|
25
|
+
<td data-label="Day worked"><%= request.date_worked.strftime("%a, %d %b %Y") %></td>
|
|
26
|
+
<td data-label="Credit" class="hrl-num"><%= request.credit_days.to_f %></td>
|
|
27
|
+
<td data-label=""><a class="hrl-btn" href="<%= hr_lite.admin_comp_off_request_path(request) %>">Open</a></td>
|
|
28
|
+
</tr>
|
|
29
|
+
<% end %>
|
|
30
|
+
</tbody>
|
|
31
|
+
</table>
|
|
32
|
+
</div>
|
|
33
|
+
<%= hrl_pagination %>
|
|
34
|
+
<% else %>
|
|
35
|
+
<p class="hrl-card--empty">No <%= @status %> comp-off requests.</p>
|
|
36
|
+
<% end %>
|
|
37
|
+
</section>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Comp-off — #{hr_display_name(@request.user)}" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Comp-off — <%= hr_display_name(@request.user) %></h1>
|
|
4
|
+
<div class="hrl-page__actions">
|
|
5
|
+
<a class="hrl-btn" href="<%= hr_lite.admin_comp_off_requests_path %>">All requests</a>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<section class="hrl-card">
|
|
10
|
+
<dl class="hrl-deflist">
|
|
11
|
+
<dt>Day worked</dt><dd><%= @request.date_worked.strftime("%A, %d %B %Y") %></dd>
|
|
12
|
+
<dt>Credit</dt><dd><%= @request.credit_days.to_f %> day</dd>
|
|
13
|
+
<dt>Reason</dt><dd><%= @request.reason %></dd>
|
|
14
|
+
<dt>Status</dt><dd><%= hrl_request_status_badge(@request.status) %></dd>
|
|
15
|
+
<dt>Punch that day</dt>
|
|
16
|
+
<dd>
|
|
17
|
+
<% if (punch = @request.punch)&.check_in_at %>
|
|
18
|
+
<%= punch.check_in_at.strftime("%H:%M") %><%= punch.check_out_at ? "–#{punch.check_out_at.strftime('%H:%M')}" : " (no check-out)" %>
|
|
19
|
+
<% else %>
|
|
20
|
+
<span class="hrl-muted">No punch recorded — weigh the reason accordingly.</span>
|
|
21
|
+
<% end %>
|
|
22
|
+
</dd>
|
|
23
|
+
<% if @request.decided_at %>
|
|
24
|
+
<dt>Decided</dt>
|
|
25
|
+
<dd><%= hr_display_name(@request.decided_by) %> · <%= @request.decided_at.strftime("%d %b %H:%M") %>
|
|
26
|
+
<% if @request.decision_note.present? %> · <%= @request.decision_note %><% end %></dd>
|
|
27
|
+
<% end %>
|
|
28
|
+
</dl>
|
|
29
|
+
</section>
|
|
30
|
+
|
|
31
|
+
<% if @request.pending? %>
|
|
32
|
+
<section class="hrl-card">
|
|
33
|
+
<h2 class="hrl-card__title">Decide</h2>
|
|
34
|
+
<%= form_with url: hr_lite.approve_admin_comp_off_request_path(@request), method: :post, local: true do |f| %>
|
|
35
|
+
<div class="hrl-field">
|
|
36
|
+
<%= f.label :decision_note, "Note (required to reject, optional to approve)" %>
|
|
37
|
+
<%= f.text_area :decision_note %>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="hrl-form-actions">
|
|
40
|
+
<%= f.button "Approve — credit #{@request.credit_days.to_f} day",
|
|
41
|
+
type: :submit, class: "hrl-btn hrl-btn--primary" %>
|
|
42
|
+
<%= f.button "Reject", type: :submit, class: "hrl-btn hrl-btn--danger",
|
|
43
|
+
formaction: hr_lite.reject_admin_comp_off_request_path(@request) %>
|
|
44
|
+
</div>
|
|
45
|
+
<% end %>
|
|
46
|
+
</section>
|
|
47
|
+
<% end %>
|
|
@@ -30,6 +30,13 @@
|
|
|
30
30
|
<% end %>
|
|
31
31
|
<div class="hrl-field"><%= f.label :employee_code %><%= f.text_field :employee_code %></div>
|
|
32
32
|
<div class="hrl-field"><%= f.label :designation %><%= f.text_field :designation %></div>
|
|
33
|
+
<div class="hrl-field">
|
|
34
|
+
<%= f.label :manager_id, "Reports to (their L1)" %>
|
|
35
|
+
<%= f.select :manager_id,
|
|
36
|
+
HrLite.active_employees.reject { |u| u.id == profile.user_id }
|
|
37
|
+
.map { |u| [ HrLite.display_name(u), u.id ] },
|
|
38
|
+
include_blank: "— nobody (top of the org) —" %>
|
|
39
|
+
</div>
|
|
33
40
|
<div class="hrl-field"><%= f.label :department %><%= f.text_field :department %></div>
|
|
34
41
|
<div class="hrl-field"><%= f.label :work_location %><%= f.text_field :work_location %></div>
|
|
35
42
|
<div class="hrl-field"><%= f.label :date_of_birth %><%= f.date_field :date_of_birth %></div>
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
<h2 class="hrl-card__title">Profile</h2>
|
|
57
57
|
<dl class="hrl-deflist">
|
|
58
58
|
<dt>Designation</dt><dd><%= @profile.designation.presence || "—" %></dd>
|
|
59
|
+
<dt>Reports to</dt><dd><%= @profile.manager ? hr_display_name(@profile.manager) : "—" %></dd>
|
|
59
60
|
<dt>Department</dt><dd><%= @profile.department.presence || "—" %></dd>
|
|
60
61
|
<dt>Joined</dt><dd><%= @profile.date_of_joining.strftime("%d %b %Y") %></dd>
|
|
61
62
|
<% if @profile.date_of_exit %><dt>Exited</dt><dd><%= @profile.date_of_exit.strftime("%d %b %Y") %></dd><% end %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<% content_for(:page_title) { "Team balances" } %>
|
|
2
2
|
<div class="hrl-page__head">
|
|
3
|
-
<h1 class="hrl-page__title">Team leave balances <%= @year %></h1>
|
|
3
|
+
<h1 class="hrl-page__title">Team leave balances <%= HrLite::LeaveYear.label(@year) %></h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
6
|
<section class="hrl-card">
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
<h1 class="hrl-page__title">Leave approvals</h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
|
+
<%= render "hr_lite/admin/shared/approvals_tabs", active: :leaves %>
|
|
7
|
+
|
|
6
8
|
<div class="hrl-row" style="margin-bottom:.8rem;">
|
|
7
9
|
<% HrLite::LeaveRequest::STATUSES.each do |status| %>
|
|
8
10
|
<a class="hrl-btn <%= 'hrl-btn--primary' if @status == status %>"
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
<div class="hrl-field"><%= f.label :position %><%= f.number_field :position %></div>
|
|
20
20
|
<div class="hrl-field hrl-field--check"><%= f.check_box :paid %><%= f.label :paid %></div>
|
|
21
21
|
<div class="hrl-field hrl-field--check"><%= f.check_box :active %><%= f.label :active %></div>
|
|
22
|
+
<div class="hrl-field hrl-field--check">
|
|
23
|
+
<%= f.check_box :comp_off %>
|
|
24
|
+
<%= f.label :comp_off, "Comp-off type (approved comp-off requests credit into this)" %>
|
|
25
|
+
</div>
|
|
22
26
|
<div class="hrl-form-actions">
|
|
23
27
|
<%= f.submit "Save", class: "hrl-btn hrl-btn--primary" %>
|
|
24
28
|
<a class="hrl-btn" href="<%= hr_lite.admin_leave_types_path %>">Cancel</a>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Regularization tickets" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Regularization tickets</h1>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<%= render "hr_lite/admin/shared/approvals_tabs", active: :regularizations %>
|
|
7
|
+
|
|
8
|
+
<div class="hrl-row" style="margin-bottom:.85rem;">
|
|
9
|
+
<span class="hrl-small hrl-muted">Status</span>
|
|
10
|
+
<% HrLite::RegularizationRequest::STATUSES.each do |status| %>
|
|
11
|
+
<a class="hrl-btn <%= 'hrl-btn--primary' if @status == status %>"
|
|
12
|
+
href="<%= hr_lite.admin_regularization_requests_path(status: status) %>"><%= status.humanize %></a>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<section class="hrl-card">
|
|
17
|
+
<% if @requests.any? %>
|
|
18
|
+
<div class="hrl-table-wrap">
|
|
19
|
+
<table class="hrl-table hrl-table--stack">
|
|
20
|
+
<thead><tr><th>Employee</th><th>Date</th><th>Proposed times</th><th></th></tr></thead>
|
|
21
|
+
<tbody>
|
|
22
|
+
<% @requests.each do |request| %>
|
|
23
|
+
<tr>
|
|
24
|
+
<td data-label="Employee"><%= hr_display_name(request.user) %></td>
|
|
25
|
+
<td data-label="Date"><%= request.date.strftime("%a, %d %b %Y") %></td>
|
|
26
|
+
<td data-label="Proposed times"><%= request.times_label %></td>
|
|
27
|
+
<td data-label=""><a class="hrl-btn" href="<%= hr_lite.admin_regularization_request_path(request) %>">Open</a></td>
|
|
28
|
+
</tr>
|
|
29
|
+
<% end %>
|
|
30
|
+
</tbody>
|
|
31
|
+
</table>
|
|
32
|
+
</div>
|
|
33
|
+
<%= hrl_pagination %>
|
|
34
|
+
<% else %>
|
|
35
|
+
<p class="hrl-card--empty">No <%= @status %> tickets.</p>
|
|
36
|
+
<% end %>
|
|
37
|
+
</section>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Ticket — #{hr_display_name(@request.user)}" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Regularization — <%= hr_display_name(@request.user) %></h1>
|
|
4
|
+
<div class="hrl-page__actions">
|
|
5
|
+
<a class="hrl-btn" href="<%= hr_lite.admin_regularization_requests_path %>">All tickets</a>
|
|
6
|
+
<a class="hrl-btn" href="<%= hr_lite.admin_attendance_path(@request.user_id, month: @request.date.strftime("%Y-%m")) %>">Their month</a>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<section class="hrl-card">
|
|
11
|
+
<dl class="hrl-deflist">
|
|
12
|
+
<dt>Date</dt><dd><%= @request.date.strftime("%A, %d %B %Y") %></dd>
|
|
13
|
+
<dt>Proposed times</dt><dd><%= @request.times_label %></dd>
|
|
14
|
+
<dt>Reason</dt><dd><%= @request.reason %></dd>
|
|
15
|
+
<dt>Status</dt><dd><%= hrl_request_status_badge(@request.status) %></dd>
|
|
16
|
+
<dt>Current punch</dt>
|
|
17
|
+
<dd>
|
|
18
|
+
<% if (punch = @request.punch)&.check_in_at %>
|
|
19
|
+
<%= punch.check_in_at.strftime("%H:%M") %><%= punch.check_out_at ? "–#{punch.check_out_at.strftime('%H:%M')}" : " (no check-out)" %>
|
|
20
|
+
<% if punch.flagged? %><span class="hrl-badge hrl-badge--bad">Flagged</span><% end %>
|
|
21
|
+
<% else %>
|
|
22
|
+
<span class="hrl-muted">No punch recorded for this day.</span>
|
|
23
|
+
<% end %>
|
|
24
|
+
</dd>
|
|
25
|
+
<% if @request.decided_at %>
|
|
26
|
+
<dt>Decided</dt>
|
|
27
|
+
<dd><%= hr_display_name(@request.decided_by) %> · <%= @request.decided_at.strftime("%d %b %H:%M") %>
|
|
28
|
+
<% if @request.decision_note.present? %> · <%= @request.decision_note %><% end %></dd>
|
|
29
|
+
<% end %>
|
|
30
|
+
</dl>
|
|
31
|
+
</section>
|
|
32
|
+
|
|
33
|
+
<% if @request.pending? %>
|
|
34
|
+
<section class="hrl-card">
|
|
35
|
+
<h2 class="hrl-card__title">Decide</h2>
|
|
36
|
+
<p class="hrl-hint">Approving writes the proposed times onto the day's attendance record
|
|
37
|
+
(only the provided times overwrite) with the full regularization trail.</p>
|
|
38
|
+
<%= form_with url: hr_lite.approve_admin_regularization_request_path(@request), method: :post, local: true do |f| %>
|
|
39
|
+
<div class="hrl-field">
|
|
40
|
+
<%= f.label :decision_note, "Note (required to reject, optional to approve)" %>
|
|
41
|
+
<%= f.text_area :decision_note %>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="hrl-form-actions">
|
|
44
|
+
<%= f.button "Approve — fix attendance", type: :submit, class: "hrl-btn hrl-btn--primary" %>
|
|
45
|
+
<%= f.button "Reject", type: :submit, class: "hrl-btn hrl-btn--danger",
|
|
46
|
+
formaction: hr_lite.reject_admin_regularization_request_path(@request) %>
|
|
47
|
+
</div>
|
|
48
|
+
<% end %>
|
|
49
|
+
</section>
|
|
50
|
+
<% end %>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%# locals: active (:leaves | :comp_off | :regularizations) %>
|
|
2
|
+
<div class="hrl-row" style="margin-bottom:.85rem;">
|
|
3
|
+
<a class="hrl-btn <%= 'hrl-btn--primary' if active == :leaves %>"
|
|
4
|
+
href="<%= hr_lite.admin_leave_requests_path %>">Leaves</a>
|
|
5
|
+
<a class="hrl-btn <%= 'hrl-btn--primary' if active == :comp_off %>"
|
|
6
|
+
href="<%= hr_lite.admin_comp_off_requests_path %>">Comp-off
|
|
7
|
+
<% if (n = HrLite::CompOffRequest.pending.count).positive? %><span class="hrl-badge hrl-badge--warn"><%= n %></span><% end %></a>
|
|
8
|
+
<a class="hrl-btn <%= 'hrl-btn--primary' if active == :regularizations %>"
|
|
9
|
+
href="<%= hr_lite.admin_regularization_requests_path %>">Regularization
|
|
10
|
+
<% if (n = HrLite::RegularizationRequest.pending.count).positive? %><span class="hrl-badge hrl-badge--warn"><%= n %></span><% end %></a>
|
|
11
|
+
</div>
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
</span>
|
|
16
16
|
</div>
|
|
17
17
|
<%= render "hr_lite/attendance/month_grid", month: @month, day_status: @day_status %>
|
|
18
|
+
<p class="hrl-small hrl-muted hrl-mt">Forgot to check in or out on some day?
|
|
19
|
+
<a href="<%= hr_lite.new_regularization_request_path %>">Raise a regularization ticket</a>
|
|
20
|
+
· <a href="<%= hr_lite.regularization_requests_path %>">My tickets</a></p>
|
|
18
21
|
</section>
|
|
19
22
|
|
|
20
23
|
<% content_for :scripts do %>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Comp-off" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">My comp-off</h1>
|
|
4
|
+
<div class="hrl-page__actions">
|
|
5
|
+
<a class="hrl-btn" href="<%= hr_lite.leave_requests_path %>">My leaves</a>
|
|
6
|
+
<a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_comp_off_request_path %>">Request comp-off</a>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<section class="hrl-card">
|
|
11
|
+
<p class="hrl-small hrl-muted" style="margin-top:0;">
|
|
12
|
+
Worked on a weekend or holiday? Request a comp-off — once an admin approves,
|
|
13
|
+
<%= @comp_off_type ? "your #{@comp_off_type.name} balance is credited" : "a day is credited to your balance" %>
|
|
14
|
+
and you spend it through the normal leave flow.
|
|
15
|
+
</p>
|
|
16
|
+
<% if @requests.any? %>
|
|
17
|
+
<div class="hrl-table-wrap">
|
|
18
|
+
<table class="hrl-table hrl-table--stack">
|
|
19
|
+
<thead><tr><th>Day worked</th><th>Credit</th><th>Status</th><th>Note</th><th></th></tr></thead>
|
|
20
|
+
<tbody>
|
|
21
|
+
<% @requests.each do |request| %>
|
|
22
|
+
<tr>
|
|
23
|
+
<td data-label="Day worked"><%= request.date_worked.strftime("%a, %d %b %Y") %></td>
|
|
24
|
+
<td data-label="Credit" class="hrl-num"><%= request.credit_days.to_f %></td>
|
|
25
|
+
<td data-label="Status"><%= hrl_request_status_badge(request.status) %></td>
|
|
26
|
+
<td data-label="Note" class="hrl-small hrl-muted"><%= request.decision_note.presence || "—" %></td>
|
|
27
|
+
<td data-label="" class="<%= 'hrl-cell--void' unless request.pending? %>">
|
|
28
|
+
<% if request.pending? %>
|
|
29
|
+
<%= button_to "Cancel", hr_lite.cancel_comp_off_request_path(request), method: :post,
|
|
30
|
+
class: "hrl-btn hrl-btn--danger", form: { data: { turbo_confirm: "Cancel this request?" } } %>
|
|
31
|
+
<% end %>
|
|
32
|
+
</td>
|
|
33
|
+
</tr>
|
|
34
|
+
<% end %>
|
|
35
|
+
</tbody>
|
|
36
|
+
</table>
|
|
37
|
+
</div>
|
|
38
|
+
<%= hrl_pagination %>
|
|
39
|
+
<% else %>
|
|
40
|
+
<p class="hrl-card--empty">No comp-off requests yet.</p>
|
|
41
|
+
<% end %>
|
|
42
|
+
</section>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Request comp-off" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Request comp-off</h1>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<section class="hrl-card">
|
|
7
|
+
<%= form_with model: @request, url: hr_lite.comp_off_requests_path, scope: :comp_off_request, local: true do |f| %>
|
|
8
|
+
<%= render "hr_lite/shared/form_errors", record: @request %>
|
|
9
|
+
<div class="hrl-field">
|
|
10
|
+
<%= f.label :date_worked, "Which day did you work?" %>
|
|
11
|
+
<%= f.date_field :date_worked, max: Date.current %>
|
|
12
|
+
<p class="hrl-hint">Must be a weekend or holiday you actually worked — extra hours on a
|
|
13
|
+
normal working day are not comp-off under the policy.</p>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="hrl-field hrl-field--check">
|
|
16
|
+
<%= f.check_box :half_day %>
|
|
17
|
+
<%= f.label :half_day, "I worked only half the day (credits 0.5)" %>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="hrl-field">
|
|
20
|
+
<%= f.label :reason, "What did you work on? (required)" %>
|
|
21
|
+
<%= f.text_area :reason %>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="hrl-form-actions">
|
|
24
|
+
<%= f.submit "Send for approval", class: "hrl-btn hrl-btn--primary" %>
|
|
25
|
+
<a class="hrl-btn" href="<%= hr_lite.comp_off_requests_path %>">Cancel</a>
|
|
26
|
+
</div>
|
|
27
|
+
<% end %>
|
|
28
|
+
</section>
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
<dl class="hrl-deflist">
|
|
9
9
|
<dt>Employee code</dt><dd><%= @profile.employee_code %></dd>
|
|
10
10
|
<dt>Designation</dt><dd><%= @profile.designation.presence || "—" %></dd>
|
|
11
|
+
<dt>Reports to</dt>
|
|
12
|
+
<dd><%= (boss = @profile.active_manager) ? "#{hr_display_name(boss)} (your L1)" : "—" %>
|
|
13
|
+
· <a href="<%= hr_lite.org_path %>">Org chart</a></dd>
|
|
11
14
|
<dt>Department</dt><dd><%= @profile.department.presence || "—" %></dd>
|
|
12
15
|
<dt>Joined</dt><dd><%= @profile.date_of_joining.strftime("%d %B %Y") %></dd>
|
|
13
16
|
<dt>Tax regime</dt><dd><%= @profile.tax_regime.humanize %></dd>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<% content_for(:page_title) { "Leave balances" } %>
|
|
2
2
|
<div class="hrl-page__head">
|
|
3
|
-
<h1 class="hrl-page__title">Leave balances <%= @year %></h1>
|
|
3
|
+
<h1 class="hrl-page__title">Leave balances <%= HrLite::LeaveYear.label(@year) %></h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
6
|
<div class="hrl-grid hrl-grid--2">
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<section class="hrl-card">
|
|
2
|
-
<h2 class="hrl-card__title">Leave balance <%=
|
|
2
|
+
<h2 class="hrl-card__title">Leave balance <%= HrLite::LeaveYear.label(HrLite::LeaveYear.current_key) %></h2>
|
|
3
3
|
<% types = HrLite::LeaveType.active.where(paid: true).where.not(annual_quota: nil) %>
|
|
4
4
|
<% if types.any? %>
|
|
5
5
|
<div class="hrl-row">
|
|
6
6
|
<% types.each do |type| %>
|
|
7
|
-
<% balance = HrLite::LeaveBalance.for(hr_current_user, type,
|
|
7
|
+
<% balance = HrLite::LeaveBalance.for(hr_current_user, type, HrLite::LeaveYear.current_key) %>
|
|
8
8
|
<span class="hrl-badge" style="background: <%= type.color %>1f; color: <%= type.color %>;">
|
|
9
9
|
<%= type.code %> <%= balance.available.to_f %>/<%= balance.entitled.to_f %>
|
|
10
10
|
</span>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<div class="hrl-page__head">
|
|
3
3
|
<h1 class="hrl-page__title">My leaves</h1>
|
|
4
4
|
<div class="hrl-page__actions">
|
|
5
|
+
<a class="hrl-btn" href="<%= hr_lite.comp_off_requests_path %>">Comp-off</a>
|
|
5
6
|
<a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_leave_request_path %>">Apply</a>
|
|
6
7
|
</div>
|
|
7
8
|
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<%# locals: profile — recursive org-chart node; @children / hr_current_user from OrgController %>
|
|
2
|
+
<li class="hrl-tree__node">
|
|
3
|
+
<div class="hrl-tree__card <%= 'hrl-tree__card--me' if profile.user_id == hr_current_user.id %>">
|
|
4
|
+
<span class="hrl-tree__name"><%= hr_display_name(profile.user) %></span>
|
|
5
|
+
<% if profile.designation.present? %><span class="hrl-tree__role"><%= profile.designation %></span><% end %>
|
|
6
|
+
<% if profile.department.present? %><span class="hrl-small hrl-muted"><%= profile.department %></span><% end %>
|
|
7
|
+
</div>
|
|
8
|
+
<% reports = @tree_children[profile.user_id].sort_by { |child| HrLite.display_name(child.user).downcase } %>
|
|
9
|
+
<% if reports.any? %>
|
|
10
|
+
<ul class="hrl-tree">
|
|
11
|
+
<%= render partial: "hr_lite/org/node", collection: reports, as: :profile %>
|
|
12
|
+
</ul>
|
|
13
|
+
<% end %>
|
|
14
|
+
</li>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Org chart" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Organisation</h1>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<% if @own_chain.any? %>
|
|
7
|
+
<section class="hrl-card">
|
|
8
|
+
<h2 class="hrl-card__title">Your reporting line</h2>
|
|
9
|
+
<div class="hrl-chainrow">
|
|
10
|
+
<span class="hrl-badge hrl-badge--ok">You</span>
|
|
11
|
+
<% @own_chain.each_with_index do |boss, index| %>
|
|
12
|
+
<span class="hrl-chainrow__step">
|
|
13
|
+
<span class="hrl-chainrow__arrow">→</span>
|
|
14
|
+
<span class="hrl-badge">
|
|
15
|
+
L<%= index + 1 %> · <%= hr_display_name(boss) %><% if (p = @by_user[boss.id]) && p.designation.present? %> (<%= p.designation %>)<% end %>
|
|
16
|
+
</span>
|
|
17
|
+
</span>
|
|
18
|
+
<% end %>
|
|
19
|
+
</div>
|
|
20
|
+
<p class="hrl-hint">L1 is your direct manager; the chain ends at the top of the organisation.</p>
|
|
21
|
+
</section>
|
|
22
|
+
<% elsif @own_profile %>
|
|
23
|
+
<section class="hrl-card">
|
|
24
|
+
<h2 class="hrl-card__title">Your reporting line</h2>
|
|
25
|
+
<p class="hrl-small hrl-muted">You are at the top of the organisation.</p>
|
|
26
|
+
</section>
|
|
27
|
+
<% end %>
|
|
28
|
+
|
|
29
|
+
<section class="hrl-card">
|
|
30
|
+
<h2 class="hrl-card__title">Who reports to whom</h2>
|
|
31
|
+
<% if @roots.any? %>
|
|
32
|
+
<ul class="hrl-tree">
|
|
33
|
+
<%= render partial: "hr_lite/org/node", collection: @roots, as: :profile %>
|
|
34
|
+
</ul>
|
|
35
|
+
<% else %>
|
|
36
|
+
<p class="hrl-card--empty">No employee profiles yet — leadership adds them under Employees.</p>
|
|
37
|
+
<% end %>
|
|
38
|
+
</section>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Regularization" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">My regularization tickets</h1>
|
|
4
|
+
<div class="hrl-page__actions">
|
|
5
|
+
<a class="hrl-btn" href="<%= hr_lite.attendance_path %>">My attendance</a>
|
|
6
|
+
<a class="hrl-btn hrl-btn--primary" href="<%= hr_lite.new_regularization_request_path %>">Raise a ticket</a>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<section class="hrl-card">
|
|
11
|
+
<p class="hrl-small hrl-muted" style="margin-top:0;">
|
|
12
|
+
Forgot to check in or out? Raise a ticket with the actual times — once an
|
|
13
|
+
admin approves, your attendance for that day is fixed automatically.
|
|
14
|
+
</p>
|
|
15
|
+
<% if @requests.any? %>
|
|
16
|
+
<div class="hrl-table-wrap">
|
|
17
|
+
<table class="hrl-table hrl-table--stack">
|
|
18
|
+
<thead><tr><th>Date</th><th>Proposed times</th><th>Status</th><th>Note</th><th></th></tr></thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<% @requests.each do |request| %>
|
|
21
|
+
<tr>
|
|
22
|
+
<td data-label="Date"><%= request.date.strftime("%a, %d %b %Y") %></td>
|
|
23
|
+
<td data-label="Proposed times"><%= request.times_label %></td>
|
|
24
|
+
<td data-label="Status"><%= hrl_request_status_badge(request.status) %></td>
|
|
25
|
+
<td data-label="Note" class="hrl-small hrl-muted"><%= request.decision_note.presence || "—" %></td>
|
|
26
|
+
<td data-label="" class="<%= 'hrl-cell--void' unless request.pending? %>">
|
|
27
|
+
<% if request.pending? %>
|
|
28
|
+
<%= button_to "Cancel", hr_lite.cancel_regularization_request_path(request), method: :post,
|
|
29
|
+
class: "hrl-btn hrl-btn--danger", form: { data: { turbo_confirm: "Cancel this ticket?" } } %>
|
|
30
|
+
<% end %>
|
|
31
|
+
</td>
|
|
32
|
+
</tr>
|
|
33
|
+
<% end %>
|
|
34
|
+
</tbody>
|
|
35
|
+
</table>
|
|
36
|
+
</div>
|
|
37
|
+
<%= hrl_pagination %>
|
|
38
|
+
<% else %>
|
|
39
|
+
<p class="hrl-card--empty">No tickets yet.</p>
|
|
40
|
+
<% end %>
|
|
41
|
+
</section>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Raise a regularization ticket" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Raise a regularization ticket</h1>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<section class="hrl-card">
|
|
7
|
+
<%= form_with model: @request, url: hr_lite.regularization_requests_path, scope: :regularization_request, local: true do |f| %>
|
|
8
|
+
<%= render "hr_lite/shared/form_errors", record: @request %>
|
|
9
|
+
<div class="hrl-field">
|
|
10
|
+
<%= f.label :date, "Which day needs fixing?" %>
|
|
11
|
+
<%= f.date_field :date, value: @request.date&.strftime("%Y-%m-%d"), max: Date.current %>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="hrl-field">
|
|
14
|
+
<%= f.label :check_in_at, "Actual check-in (leave blank if only check-out is missing)" %>
|
|
15
|
+
<%= f.datetime_local_field :check_in_at, value: @request.check_in_at&.strftime("%Y-%m-%dT%H:%M"), max: Date.current.strftime("%Y-%m-%dT23:59") %>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="hrl-field">
|
|
18
|
+
<%= f.label :check_out_at, "Actual check-out (leave blank if only check-in is missing)" %>
|
|
19
|
+
<%= f.datetime_local_field :check_out_at, value: @request.check_out_at&.strftime("%Y-%m-%dT%H:%M"), max: Date.current.strftime("%Y-%m-%dT23:59") %>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="hrl-field">
|
|
22
|
+
<%= f.label :reason, "Why was the punch missed? (required)" %>
|
|
23
|
+
<%= f.text_area :reason %>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="hrl-form-actions">
|
|
26
|
+
<%= f.submit "Raise ticket", class: "hrl-btn hrl-btn--primary" %>
|
|
27
|
+
<a class="hrl-btn" href="<%= hr_lite.regularization_requests_path %>">Cancel</a>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
30
|
+
</section>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<% content_for(:page_title) { "Team" } %>
|
|
2
|
+
<div class="hrl-page__head">
|
|
3
|
+
<h1 class="hrl-page__title">Team — <%= @date == Date.current ? "today" : @date.strftime("%A, %d %B") %></h1>
|
|
4
|
+
<div class="hrl-page__actions">
|
|
5
|
+
<a class="hrl-btn" aria-label="Previous day" href="<%= hr_lite.team_path(date: (@date - 1).strftime("%Y-%m-%d")) %>">←</a>
|
|
6
|
+
<% unless @date == Date.current %>
|
|
7
|
+
<a class="hrl-btn" href="<%= hr_lite.team_path %>">Today</a>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% if @date < Date.current %>
|
|
10
|
+
<a class="hrl-btn" aria-label="Next day" href="<%= hr_lite.team_path(date: (@date + 1).strftime("%Y-%m-%d")) %>">→</a>
|
|
11
|
+
<% end %>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<% kpis = @board.kpis %>
|
|
16
|
+
<div class="hrl-kpis">
|
|
17
|
+
<div class="hrl-kpi"><div class="hrl-kpi__value"><%= kpis[:checked_in] %></div><div class="hrl-kpi__label">Checked in</div></div>
|
|
18
|
+
<div class="hrl-kpi"><div class="hrl-kpi__value"><%= kpis[:on_leave] %></div><div class="hrl-kpi__label">On leave</div></div>
|
|
19
|
+
<div class="hrl-kpi"><div class="hrl-kpi__value"><%= kpis[:not_in] %></div><div class="hrl-kpi__label">Not in</div></div>
|
|
20
|
+
<div class="hrl-kpi"><div class="hrl-kpi__value"><%= @board.rows.size %></div><div class="hrl-kpi__label">Team size</div></div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<section class="hrl-card">
|
|
24
|
+
<% if @board.rows.any? %>
|
|
25
|
+
<div class="hrl-table-wrap">
|
|
26
|
+
<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>
|
|
28
|
+
<tbody>
|
|
29
|
+
<% @board.rows.each do |row| %>
|
|
30
|
+
<tr>
|
|
31
|
+
<td data-label="Name">
|
|
32
|
+
<span><strong><%= hr_display_name(row.user) %></strong><% if row.profile&.designation.present? %>
|
|
33
|
+
<span class="hrl-small hrl-muted"> · <%= row.profile.designation %></span><% end %></span>
|
|
34
|
+
</td>
|
|
35
|
+
<td data-label="Status"><%= hrl_team_status(row) %></td>
|
|
36
|
+
<td data-label="Hours" class="hrl-num"><%= hrl_duration(row.day_seconds) %></td>
|
|
37
|
+
<td data-label="Hours this month" class="hrl-num"><%= hrl_duration(row.month_seconds) %></td>
|
|
38
|
+
</tr>
|
|
39
|
+
<% end %>
|
|
40
|
+
</tbody>
|
|
41
|
+
</table>
|
|
42
|
+
</div>
|
|
43
|
+
<% else %>
|
|
44
|
+
<p class="hrl-card--empty">Nobody to show for this date.</p>
|
|
45
|
+
<% end %>
|
|
46
|
+
<p class="hrl-small hrl-muted hrl-mt">Forgot a punch yourself?
|
|
47
|
+
<a href="<%= hr_lite.new_regularization_request_path %>">Raise a regularization ticket</a>.</p>
|
|
48
|
+
</section>
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
<% end %>
|
|
32
32
|
|
|
33
33
|
<% if hr_admin? || hr_leadership? %>
|
|
34
|
-
<span class="hrl-side__group">
|
|
34
|
+
<span class="hrl-side__group">Manage</span>
|
|
35
35
|
<% hrl_nav_items(HrLite::ApplicationHelper::ADMIN_NAV_ITEMS).each do |item| %>
|
|
36
36
|
<%= hrl_nav_link(item) %>
|
|
37
37
|
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -13,6 +13,14 @@ HrLite::Engine.routes.draw do
|
|
|
13
13
|
member { post :cancel }
|
|
14
14
|
end
|
|
15
15
|
resources :leave_balances, only: :index
|
|
16
|
+
resources :comp_off_requests, only: %i[index new create] do
|
|
17
|
+
member { post :cancel }
|
|
18
|
+
end
|
|
19
|
+
resources :regularization_requests, only: %i[index new create] do
|
|
20
|
+
member { post :cancel }
|
|
21
|
+
end
|
|
22
|
+
get "team", to: "team#show"
|
|
23
|
+
get "org", to: "org#show"
|
|
16
24
|
resources :holidays, only: :index
|
|
17
25
|
get "calendar", to: "calendar#show"
|
|
18
26
|
resources :salary_slips, only: %i[index show]
|
|
@@ -32,6 +40,12 @@ HrLite::Engine.routes.draw do
|
|
|
32
40
|
resources :leave_balances, only: :index do
|
|
33
41
|
collection { post :adjust }
|
|
34
42
|
end
|
|
43
|
+
resources :comp_off_requests, only: %i[index show] do
|
|
44
|
+
member { post :approve; post :reject }
|
|
45
|
+
end
|
|
46
|
+
resources :regularization_requests, only: %i[index show] do
|
|
47
|
+
member { post :approve; post :reject }
|
|
48
|
+
end
|
|
35
49
|
resources :leave_types, except: :show
|
|
36
50
|
resources :office_locations, except: :show
|
|
37
51
|
resources :holidays, only: %i[index create update destroy] do
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class CreateHrLiteCompOffRequests < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :hr_lite_comp_off_requests do |t|
|
|
4
|
+
t.bigint :user_id, null: false
|
|
5
|
+
t.date :date_worked, null: false
|
|
6
|
+
t.boolean :half_day, null: false, default: 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
|
+
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
add_index :hr_lite_comp_off_requests, [ :user_id, :status ]
|
|
16
|
+
add_index :hr_lite_comp_off_requests, [ :user_id, :date_worked ]
|
|
17
|
+
add_index :hr_lite_comp_off_requests, [ :status, :date_worked ]
|
|
18
|
+
# Two tabs / a double-tap racing past the app-level duplicate check
|
|
19
|
+
# must not create two live requests for one worked day.
|
|
20
|
+
add_index :hr_lite_comp_off_requests, [ :user_id, :date_worked ],
|
|
21
|
+
unique: true, where: "status IN ('pending', 'approved')",
|
|
22
|
+
name: "index_hr_lite_comp_off_requests_live_uniqueness"
|
|
23
|
+
end
|
|
24
|
+
end
|