hr_lite 0.21.3 → 0.22.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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -0
  3. data/README.md +54 -2
  4. data/app/assets/javascripts/hr_lite/confirm.js +18 -3
  5. data/app/assets/javascripts/hr_lite/geo_punch.js +1 -1
  6. data/app/assets/javascripts/hr_lite/nav.js +17 -0
  7. data/app/assets/javascripts/hr_lite/select_filter.js +46 -0
  8. data/app/assets/stylesheets/hr_lite/hr_lite.css +217 -79
  9. data/app/controllers/hr_lite/admin/assets_controller.rb +1 -1
  10. data/app/controllers/hr_lite/admin/attendances_controller.rb +31 -0
  11. data/app/controllers/hr_lite/approvals_controller.rb +1 -12
  12. data/app/controllers/hr_lite/attendance_controller.rb +7 -0
  13. data/app/controllers/hr_lite/holidays_controller.rb +1 -0
  14. data/app/controllers/hr_lite/home_controller.rb +40 -3
  15. data/app/controllers/hr_lite/hr_requests_controller.rb +1 -1
  16. data/app/controllers/hr_lite/leave_requests_controller.rb +13 -2
  17. data/app/controllers/hr_lite/regularization_requests_controller.rb +10 -2
  18. data/app/controllers/hr_lite/tax_declarations_controller.rb +4 -0
  19. data/app/helpers/hr_lite/application_helper.rb +43 -30
  20. data/app/jobs/hr_lite/attendance_close_job.rb +6 -1
  21. data/app/models/hr_lite/approval.rb +5 -0
  22. data/app/models/hr_lite/attendance_record.rb +26 -0
  23. data/app/models/hr_lite/employee_profile.rb +1 -0
  24. data/app/models/hr_lite/hr_request.rb +2 -1
  25. data/app/models/hr_lite/office_location.rb +12 -0
  26. data/app/models/hr_lite/regularization_request.rb +10 -4
  27. data/app/services/hr_lite/attendance_puncher.rb +8 -3
  28. data/app/views/hr_lite/admin/assets/index.html.erb +3 -2
  29. data/app/views/hr_lite/admin/assets/new.html.erb +6 -2
  30. data/app/views/hr_lite/admin/attendances/index.html.erb +37 -6
  31. data/app/views/hr_lite/admin/attendances/show.html.erb +11 -7
  32. data/app/views/hr_lite/admin/attendances/week.html.erb +42 -0
  33. data/app/views/hr_lite/admin/checklists/index.html.erb +1 -1
  34. data/app/views/hr_lite/admin/comp_off_requests/index.html.erb +2 -2
  35. data/app/views/hr_lite/admin/comp_off_requests/show.html.erb +2 -2
  36. data/app/views/hr_lite/admin/documents/index.html.erb +1 -1
  37. data/app/views/hr_lite/admin/employees/_form.html.erb +15 -7
  38. data/app/views/hr_lite/admin/employees/show.html.erb +5 -5
  39. data/app/views/hr_lite/admin/expenses/index.html.erb +2 -2
  40. data/app/views/hr_lite/admin/leave_balances/index.html.erb +3 -3
  41. data/app/views/hr_lite/admin/leave_requests/index.html.erb +2 -2
  42. data/app/views/hr_lite/admin/leave_requests/show.html.erb +4 -6
  43. data/app/views/hr_lite/admin/loans/new.html.erb +2 -1
  44. data/app/views/hr_lite/admin/overview/index.html.erb +3 -3
  45. data/app/views/hr_lite/admin/regularization_requests/index.html.erb +1 -1
  46. data/app/views/hr_lite/admin/shared/_approvals_tabs.html.erb +6 -5
  47. data/app/views/hr_lite/appraisals/show.html.erb +1 -1
  48. data/app/views/hr_lite/approvals/index.html.erb +1 -1
  49. data/app/views/hr_lite/attendance/_month_grid.html.erb +21 -26
  50. data/app/views/hr_lite/attendance/_punch_card.html.erb +29 -7
  51. data/app/views/hr_lite/attendance/show.html.erb +36 -7
  52. data/app/views/hr_lite/benefits/index.html.erb +1 -1
  53. data/app/views/hr_lite/calendar/show.html.erb +29 -22
  54. data/app/views/hr_lite/comp_off_requests/index.html.erb +3 -3
  55. data/app/views/hr_lite/comp_off_requests/new.html.erb +3 -2
  56. data/app/views/hr_lite/documents/index.html.erb +32 -30
  57. data/app/views/hr_lite/employee_profiles/show.html.erb +2 -2
  58. data/app/views/hr_lite/expenses/index.html.erb +17 -35
  59. data/app/views/hr_lite/expenses/new.html.erb +11 -6
  60. data/app/views/hr_lite/holidays/_next_holiday.html.erb +11 -8
  61. data/app/views/hr_lite/holidays/index.html.erb +6 -6
  62. data/app/views/hr_lite/home/index.html.erb +29 -19
  63. data/app/views/hr_lite/hr_requests/index.html.erb +7 -24
  64. data/app/views/hr_lite/hr_requests/new.html.erb +7 -5
  65. data/app/views/hr_lite/hr_requests/show.html.erb +4 -0
  66. data/app/views/hr_lite/kudos/index.html.erb +4 -3
  67. data/app/views/hr_lite/leave_balances/index.html.erb +14 -5
  68. data/app/views/hr_lite/leave_requests/_balance_chips.html.erb +5 -3
  69. data/app/views/hr_lite/leave_requests/_out_list.html.erb +16 -0
  70. data/app/views/hr_lite/leave_requests/index.html.erb +8 -21
  71. data/app/views/hr_lite/leave_requests/new.html.erb +22 -9
  72. data/app/views/hr_lite/leave_requests/show.html.erb +3 -5
  73. data/app/views/hr_lite/loans/index.html.erb +18 -20
  74. data/app/views/hr_lite/policies/index.html.erb +1 -1
  75. data/app/views/hr_lite/regularization_requests/index.html.erb +2 -2
  76. data/app/views/hr_lite/regularization_requests/new.html.erb +11 -8
  77. data/app/views/hr_lite/resignations/show.html.erb +1 -1
  78. data/app/views/hr_lite/salary_slips/_slip_detail.html.erb +9 -3
  79. data/app/views/hr_lite/salary_slips/index.html.erb +48 -44
  80. data/app/views/hr_lite/shared/_bell.html.erb +1 -1
  81. data/app/views/hr_lite/shared/_form_errors.html.erb +1 -1
  82. data/app/views/hr_lite/tax_declarations/show.html.erb +24 -19
  83. data/app/views/hr_lite/team/show.html.erb +2 -2
  84. data/app/views/layouts/hr_lite/application.html.erb +24 -8
  85. data/config/routes.rb +3 -1
  86. data/lib/hr_lite/configuration.rb +16 -2
  87. data/lib/hr_lite/notifications.rb +1 -0
  88. data/lib/hr_lite/version.rb +1 -1
  89. metadata +4 -1
@@ -3,24 +3,68 @@
3
3
  overriding the variables below (see README), never by editing this file. */
4
4
 
5
5
  :root {
6
+ /* Type. rem so OS text scaling works; nothing renders below 12px. */
6
7
  --hrl-font: -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
8
+ --hrl-font-head: var(--hrl-font);
9
+ --hrl-t-micro: .75rem;
10
+ --hrl-t-small: .875rem;
11
+ --hrl-t-body: 1rem;
12
+ --hrl-t-lead: 1.125rem;
13
+ --hrl-t-title: 1.25rem;
14
+ --hrl-t-display: 1.5rem;
15
+ --hrl-lh-tight: 1.25;
16
+ --hrl-lh-body: 1.5;
17
+ /* Colour roles. */
7
18
  --hrl-accent: #2563eb;
8
19
  --hrl-accent-ink: #ffffff;
9
20
  --hrl-ink: #111827;
10
- --hrl-muted: #6b7280;
21
+ --hrl-ink-2: #374151;
22
+ --hrl-muted: #4b5563;
11
23
  --hrl-bg: #f4f5f7;
12
24
  --hrl-card: #ffffff;
13
25
  --hrl-line: #e5e7eb;
26
+ --hrl-field: #6b7280;
14
27
  --hrl-ok: #047857;
15
28
  --hrl-ok-bg: #d1fae5;
16
29
  --hrl-warn: #b45309;
17
30
  --hrl-warn-bg: #fef3c7;
18
31
  --hrl-bad: #b91c1c;
19
32
  --hrl-bad-bg: #fee2e2;
20
- --hrl-info-bg: #dbeafe;
21
- --hrl-radius: 14px;
22
- --hrl-radius-sm: 9px;
33
+ --hrl-info-bg: #eff6ff;
34
+ --hrl-scrim: rgba(15, 23, 42, .32);
35
+ /* Spacing. */
36
+ --hrl-s-1: 4px;
37
+ --hrl-s-2: 8px;
38
+ --hrl-s-3: 12px;
39
+ --hrl-s-4: 16px;
40
+ --hrl-s-5: 20px;
41
+ --hrl-s-6: 24px;
42
+ --hrl-s-8: 32px;
43
+ --hrl-s-10: 40px;
44
+ --hrl-s-12: 48px;
45
+ --hrl-gutter: .85rem;
46
+ --hrl-target: 44px;
47
+ --hrl-gap-target: 8px;
48
+ /* Radii. */
49
+ --hrl-radius-sm: 12px;
50
+ --hrl-radius: 16px;
51
+ --hrl-radius-lg: 20px;
52
+ --hrl-radius-pill: 9999px;
53
+ /* Elevation. */
23
54
  --hrl-shadow: 0 1px 3px rgba(17, 24, 39, .07);
55
+ --hrl-shadow-md: 0 6px 18px rgba(15, 23, 42, .08);
56
+ --hrl-shadow-lg: 0 18px 40px rgba(15, 23, 42, .14);
57
+ --hrl-shadow-sheet: 0 -6px 18px rgba(15, 23, 42, .08);
58
+ /* Focus, motion, layering. */
59
+ --hrl-focus: var(--hrl-accent);
60
+ --hrl-focus-w: 2px;
61
+ --hrl-focus-offset: 2px;
62
+ --hrl-ease: cubic-bezier(.2, .6, .2, 1);
63
+ --hrl-dur-fast: 120ms;
64
+ --hrl-dur: 220ms;
65
+ --hrl-z-sticky: 20;
66
+ --hrl-z-menu: 30;
67
+ --hrl-z-sheet: 40;
24
68
  }
25
69
 
26
70
  * { box-sizing: border-box; }
@@ -29,6 +73,7 @@
29
73
  font-family: var(--hrl-font);
30
74
  color: var(--hrl-ink);
31
75
  background: var(--hrl-bg);
76
+ line-height: var(--hrl-lh-body);
32
77
  -webkit-font-smoothing: antialiased;
33
78
  overflow-x: hidden;
34
79
  }
@@ -36,6 +81,24 @@
36
81
  (.hrl-btn--primary, .hrl-nav__link, ...) can set its own link colour —
37
82
  a plain `.hrl-body a` would out-rank them and paint accent-on-accent. */
38
83
  :where(.hrl-body) a { color: var(--hrl-accent); text-decoration: none; }
84
+ /* Keyboard focus: one ring everywhere (the second selector is a date input's picker button, focused inside its shadow);
85
+ scroll padding keeps it clear of the sticky app bar and fixed tab bar. */
86
+ :where(.hrl-body) :focus-visible,
87
+ :where(.hrl-body) input:focus-within:not(:focus) { outline: var(--hrl-focus-w) solid var(--hrl-focus); outline-offset: var(--hrl-focus-offset); }
88
+ html:has(.hrl-body) { scroll-padding-top: 4rem; scroll-padding-bottom: calc(4.4rem + env(safe-area-inset-bottom)); }
89
+ .hrl-sr-only, .hrl-skip:not(:focus) {
90
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
91
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
92
+ }
93
+ .hrl-skip:focus {
94
+ position: fixed; top: var(--hrl-s-2); left: var(--hrl-s-2); z-index: var(--hrl-z-sheet);
95
+ display: inline-flex; align-items: center; min-height: var(--hrl-target); padding: 0 var(--hrl-s-4);
96
+ background: var(--hrl-card); color: var(--hrl-accent); font-weight: 600;
97
+ border-radius: var(--hrl-radius-sm); box-shadow: var(--hrl-shadow-md);
98
+ }
99
+ @media (prefers-reduced-motion: reduce) {
100
+ .hrl-body *, .hrl-body *::before, .hrl-body *::after { transition: none !important; animation: none !important; scroll-behavior: auto !important; }
101
+ }
39
102
  .hrl-shell { display: flex; min-height: 100vh; }
40
103
  .hrl-content { flex: 1; min-width: 0; }
41
104
  .hrl-main {
@@ -49,8 +112,8 @@
49
112
  /* Left rail (Keka-style) — hidden on phones, fixed column on >=768px */
50
113
  .hrl-side { display: none; }
51
114
  .hrl-nav__link {
52
- color: var(--hrl-muted); font-size: .88rem; padding: .5rem .6rem;
53
- border-radius: var(--hrl-radius-sm); min-height: 44px;
115
+ color: var(--hrl-muted); font-size: var(--hrl-t-small); padding: .5rem .6rem;
116
+ border-radius: var(--hrl-radius-sm); min-height: var(--hrl-target);
54
117
  display: inline-flex; align-items: center;
55
118
  }
56
119
  .hrl-nav__link--active { color: var(--hrl-accent); background: var(--hrl-info-bg); font-weight: 600; }
@@ -59,50 +122,59 @@
59
122
  .hrl-appbar--mobile {
60
123
  background: var(--hrl-card);
61
124
  border-bottom: 1px solid var(--hrl-line);
62
- padding: calc(.6rem + env(safe-area-inset-top)) .85rem .6rem;
63
- position: sticky; top: 0; z-index: 20;
125
+ padding: calc(var(--hrl-s-2) + env(safe-area-inset-top)) var(--hrl-gutter) var(--hrl-s-2);
126
+ position: sticky; top: 0; z-index: var(--hrl-z-sticky);
64
127
  display: flex; align-items: center; gap: .75rem;
65
128
  }
66
- .hrl-appbar__brand { font-weight: 700; font-size: 1.02rem; color: var(--hrl-ink); display: inline-flex; align-items: center; }
67
- .hrl-appbar__logo { height: 28px; width: auto; display: block; }
68
- .hrl-appbar__user { margin-left: auto; color: var(--hrl-muted); font-size: .82rem; white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
129
+ .hrl-appbar__brand { font-weight: 700; font-size: var(--hrl-t-body); color: var(--hrl-ink); line-height: var(--hrl-target); flex: 0 1 auto; min-width: var(--hrl-target); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
130
+ .hrl-appbar__logo { height: 28px; width: auto; vertical-align: middle; }
131
+ .hrl-appbar__user { margin-left: auto; color: var(--hrl-muted); font-size: var(--hrl-t-small); line-height: var(--hrl-target); white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
69
132
 
70
133
  /* Bottom tab bar (phone) */
71
134
  .hrl-tabbar {
72
- position: fixed; bottom: 0; left: 0; right: 0; z-index: 20;
73
- display: flex; justify-content: space-around;
135
+ position: fixed; bottom: 0; left: 0; right: 0; z-index: var(--hrl-z-sticky);
136
+ display: flex; justify-content: space-around; gap: var(--hrl-gap-target);
74
137
  background: var(--hrl-card); border-top: 1px solid var(--hrl-line);
75
138
  padding: .25rem .25rem calc(.25rem + env(safe-area-inset-bottom));
76
139
  }
77
- .hrl-tabbar .hrl-nav__link { flex: 1; justify-content: center; font-size: .78rem; }
140
+ .hrl-tabbar .hrl-nav__link { flex: 1 1 auto; min-width: var(--hrl-target); padding-inline: var(--hrl-s-1); justify-content: center; font-size: var(--hrl-t-micro); font-weight: 600; }
78
141
  /* "More" sheet — everything five tabs cannot hold. Pure <details>: the
79
142
  engine ships no Turbo or Stimulus and this must not need any. */
80
- .hrl-tabbar__more { flex: 1; display: flex; }
143
+ .hrl-tabbar__more { flex: 1 1 auto; display: flex; }
81
144
  .hrl-tabbar__more > summary {
82
145
  flex: 1; justify-content: center; list-style: none; cursor: pointer;
83
146
  }
84
147
  .hrl-tabbar__more > summary::-webkit-details-marker { display: none; }
85
- .hrl-tabbar__more[open] > summary { color: var(--hrl-accent); font-weight: 600; }
148
+ .hrl-tabbar__more[open] > summary,
149
+ .hrl-tabbar__more > summary.hrl-nav__link--active { color: var(--hrl-accent); font-weight: 600; }
86
150
  .hrl-sheet {
87
151
  position: fixed; left: 0; right: 0;
88
152
  bottom: calc(3.3rem + env(safe-area-inset-bottom));
89
- max-height: 60vh; overflow-y: auto;
90
- display: flex; flex-direction: column; gap: 2px;
153
+ max-height: calc(100vh - 8rem - env(safe-area-inset-top) - env(safe-area-inset-bottom)); overflow-y: auto;
154
+ display: grid; grid-template-columns: 1fr 1fr; gap: 2px var(--hrl-gap-target);
91
155
  background: var(--hrl-card); border-top: 1px solid var(--hrl-line);
156
+ border-radius: var(--hrl-radius-lg) var(--hrl-radius-lg) 0 0;
92
157
  padding: .6rem .85rem;
93
- box-shadow: 0 -6px 18px rgb(0 0 0 / 8%);
158
+ box-shadow: var(--hrl-shadow-sheet);
94
159
  }
95
160
  /* Left-aligned like the desktop rail — the tab-bar rule centres its own
96
161
  links and would otherwise apply here too. */
97
162
  .hrl-sheet .hrl-nav__link {
98
163
  display: flex; justify-content: flex-start;
99
- min-height: 44px; font-size: .92rem; padding: .5rem .6rem;
164
+ min-height: var(--hrl-target); font-size: var(--hrl-t-body); padding: .5rem .6rem;
100
165
  }
101
166
  .hrl-sheet .hrl-side__group {
102
- color: var(--hrl-muted); font-size: .72rem; font-weight: 700;
167
+ color: var(--hrl-muted); font-size: var(--hrl-t-micro); font-weight: 700;
103
168
  text-transform: uppercase; letter-spacing: .06em;
104
169
  margin: .7rem 0 .15rem; padding: 0 .6rem;
105
170
  }
171
+ .hrl-sheet .hrl-side__group, .hrl-sheet .hrl-side__back { grid-column: 1 / -1; }
172
+ @media (min-width: 481px) { .hrl-sheet { grid-template-columns: 1fr; } }
173
+ /* Scrim over the page while the sheet is open; tapping it closes the sheet (nav.js). */
174
+ .hrl-tabbar__more[open]::before {
175
+ content: ""; position: fixed; inset: 0 0 calc(3.3rem + env(safe-area-inset-bottom));
176
+ background: var(--hrl-scrim); z-index: -1;
177
+ }
106
178
 
107
179
  @media (min-width: 768px) {
108
180
  .hrl-tabbar { display: none; }
@@ -116,13 +188,13 @@
116
188
  position: sticky; top: 0; height: 100vh; overflow-y: auto;
117
189
  padding: 1rem .75rem;
118
190
  }
119
- .hrl-side__brand { display: block; padding: .25rem .5rem 1rem; }
191
+ .hrl-side__brand { display: flex; align-items: center; min-height: var(--hrl-target); padding: .25rem .5rem 1rem; }
120
192
  .hrl-side__logo { max-height: 40px; max-width: 100%; display: block; }
121
- .hrl-side__brandname { font-weight: 800; font-size: 1.1rem; color: var(--hrl-ink); }
193
+ .hrl-side__brandname { font-family: var(--hrl-font-head); font-weight: 700; font-size: var(--hrl-t-lead); color: var(--hrl-ink); }
122
194
  .hrl-side__nav { display: flex; flex-direction: column; gap: 2px; }
123
- .hrl-side__nav .hrl-nav__link { display: flex; min-height: 38px; padding: .45rem .6rem; }
195
+ .hrl-side__nav .hrl-nav__link { display: flex; padding: .45rem .6rem; }
124
196
  .hrl-side__group {
125
- margin: .9rem .5rem .25rem; font-size: .68rem; font-weight: 700;
197
+ margin: .9rem .5rem .25rem; font-size: var(--hrl-t-micro); font-weight: 700;
126
198
  letter-spacing: .08em; text-transform: uppercase; color: var(--hrl-muted);
127
199
  }
128
200
  .hrl-side__group:first-child { margin-top: 0; }
@@ -130,20 +202,27 @@
130
202
  margin-top: auto; padding: .75rem .5rem 0; border-top: 1px solid var(--hrl-line);
131
203
  display: flex; flex-direction: column; gap: .35rem;
132
204
  }
133
- .hrl-side__user { font-size: .82rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
134
- .hrl-side__back { font-size: .8rem; }
205
+ .hrl-side__user { font-size: var(--hrl-t-small); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
206
+ .hrl-side__back { display: flex; align-items: center; min-height: var(--hrl-target); font-size: var(--hrl-t-small); }
135
207
  }
136
208
 
137
209
  /* Flash */
138
- .hrl-flash { padding: .7rem .9rem; border-radius: var(--hrl-radius-sm); margin-bottom: .85rem; font-size: .9rem; overflow-wrap: anywhere; }
210
+ .hrl-flash { padding: .7rem .9rem; border-radius: var(--hrl-radius-sm); margin-bottom: .85rem; font-size: var(--hrl-t-body); overflow-wrap: anywhere; }
139
211
  .hrl-flash--ok { background: var(--hrl-ok-bg); color: var(--hrl-ok); }
140
212
  .hrl-flash--warn { background: var(--hrl-warn-bg); color: var(--hrl-warn); }
141
213
 
142
214
  /* Page scaffold */
143
215
  .hrl-page__head { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; margin: .25rem 0 .9rem; }
144
- .hrl-page__title { font-size: 1.25rem; font-weight: 700; margin: 0; overflow-wrap: anywhere; }
145
- .hrl-page__sub { color: var(--hrl-muted); font-size: .86rem; margin: .15rem 0 0; width: 100%; }
146
- .hrl-page__actions { margin-left: auto; display: flex; flex-wrap: wrap; gap: .5rem; }
216
+ .hrl-page__title { font-family: var(--hrl-font-head); font-size: var(--hrl-t-title); font-weight: 700; line-height: var(--hrl-lh-tight); margin: 0; overflow-wrap: anywhere; }
217
+ @media (min-width: 768px) { .hrl-page__title { font-size: var(--hrl-t-display); } }
218
+ .hrl-page__sub { color: var(--hrl-muted); font-size: var(--hrl-t-small); margin: .15rem 0 0; width: 100%; }
219
+ .hrl-page__actions { width: 100%; display: flex; flex-wrap: wrap; gap: .5rem; }
220
+ .hrl-page__actions .hrl-btn { flex: 1 1 auto; }
221
+ .hrl-page__actions .hrl-btn[aria-label] { flex: 0 0 auto; } /* icon-only pager arrows keep their width */
222
+ @media (min-width: 481px) {
223
+ .hrl-page__actions { width: auto; margin-left: auto; }
224
+ .hrl-page__actions .hrl-btn { flex: 0 1 auto; }
225
+ }
147
226
 
148
227
  /* Cards */
149
228
  .hrl-card {
@@ -151,124 +230,163 @@
151
230
  border-radius: var(--hrl-radius); box-shadow: var(--hrl-shadow);
152
231
  padding: .95rem; margin-bottom: .85rem;
153
232
  }
154
- .hrl-card__title { font-weight: 650; font-size: .95rem; margin: 0 0 .6rem; }
233
+ .hrl-card__title { font-family: var(--hrl-font-head); font-weight: 700; font-size: var(--hrl-t-lead); line-height: var(--hrl-lh-tight); margin: 0 0 .6rem; }
155
234
  .hrl-card--empty { text-align: center; color: var(--hrl-muted); padding: 1.8rem 1rem; }
156
- .hrl-grid { display: grid; gap: .85rem; grid-template-columns: 1fr; }
235
+ .hrl-card--warn { background: var(--hrl-warn-bg); border-color: transparent; }
236
+ .hrl-card > p:first-child { margin-top: 0; }
237
+ .hrl-card__link, .hrl-backlink { display: inline-flex; gap: var(--hrl-s-1); align-items: center; min-height: var(--hrl-target); min-width: var(--hrl-target); font-size: var(--hrl-t-small); font-weight: 600; }
238
+ .hrl-listrow { display: grid; grid-template-columns: 1fr auto; align-items: start; gap: var(--hrl-s-1) var(--hrl-s-3); padding: var(--hrl-s-3) 0; border-bottom: 1px solid var(--hrl-line); min-height: var(--hrl-target); color: var(--hrl-ink); overflow-wrap: anywhere; }
239
+ .hrl-listrow:last-child { border-bottom: 0; }
240
+ .hrl-listrow > :first-child { display: inline-flex; align-items: center; gap: var(--hrl-s-1); line-height: var(--hrl-lh-tight); }
241
+ /* The punch status stays in the accessibility tree (no `hidden`) so its first message is announced; empty, it takes no space. */
242
+ [data-hrl-geo-status]:empty { margin: 0; }
243
+ .hrl-grid { display: grid; gap: .85rem; grid-template-columns: 1fr; margin-bottom: .85rem; }
157
244
  .hrl-grid > * { min-width: 0; }
158
- @media (min-width: 700px) { .hrl-grid--2 { grid-template-columns: 1fr 1fr; } }
245
+ .hrl-grid > .hrl-card { margin-bottom: 0; }
246
+ @media (min-width: 1024px) { .hrl-grid--2 { grid-template-columns: 1fr 1fr; } }
159
247
 
160
248
  /* KPI tiles */
161
249
  .hrl-kpis { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .6rem; margin-bottom: .85rem; }
162
250
  @media (min-width: 1100px) { .hrl-kpis { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
163
251
  .hrl-kpi { background: var(--hrl-card); border: 1px solid var(--hrl-line); border-radius: var(--hrl-radius); padding: .7rem .8rem; }
164
- .hrl-kpi__value { font-size: 1.35rem; font-weight: 700; overflow-wrap: anywhere; }
165
- .hrl-kpi__label { color: var(--hrl-muted); font-size: .78rem; }
252
+ .hrl-kpi__value { font-size: var(--hrl-t-display); font-weight: 700; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
253
+ .hrl-kpi__label { color: var(--hrl-muted); font-size: var(--hrl-t-small); }
254
+ /* Rupee tiles: a lakh amount fits a half-width 320px tile without splitting its digits. */
255
+ .hrl-kpis--money .hrl-kpi__value { font-size: var(--hrl-t-lead); }
256
+ @media (min-width: 481px) { .hrl-kpis--money .hrl-kpi__value { font-size: var(--hrl-t-display); } }
166
257
 
167
258
  /* Buttons */
168
259
  .hrl-btn {
169
260
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
170
- min-height: 44px; padding: .55rem 1rem; border-radius: var(--hrl-radius-sm);
261
+ min-height: var(--hrl-target); padding: .55rem 1rem; border-radius: var(--hrl-radius-sm);
171
262
  border: 1px solid var(--hrl-line); background: var(--hrl-card); color: var(--hrl-ink);
172
- font-size: .9rem; font-weight: 600; cursor: pointer; text-decoration: none;
263
+ font-size: var(--hrl-t-body); font-weight: 600; cursor: pointer; text-decoration: none;
264
+ max-width: 100%; overflow-wrap: anywhere; text-align: center;
173
265
  }
174
266
  .hrl-btn--primary { background: var(--hrl-accent); border-color: var(--hrl-accent); color: var(--hrl-accent-ink); }
175
267
  .hrl-btn--danger { color: var(--hrl-bad); border-color: var(--hrl-bad-bg); }
176
268
  .hrl-btn--block { width: 100%; }
177
- .hrl-btn--big { min-height: 58px; font-size: 1.05rem; }
269
+ .hrl-btn--big { min-height: 58px; font-size: var(--hrl-t-lead); }
178
270
  .hrl-btn[disabled] { opacity: .55; cursor: default; }
271
+ summary.hrl-btn { list-style: none; }
272
+ summary.hrl-btn::-webkit-details-marker { display: none; } /* Safari before 18.4 draws it whatever the display */
179
273
 
180
274
  /* Badges & chips */
181
275
  .hrl-badge {
182
- display: inline-flex; align-items: center; gap: .3rem;
183
- font-size: .74rem; font-weight: 650; padding: .18rem .55rem;
184
- border-radius: 999px; background: var(--hrl-info-bg); color: var(--hrl-accent);
185
- white-space: nowrap;
276
+ display: inline-block;
277
+ font-size: var(--hrl-t-micro); font-weight: 600; line-height: var(--hrl-lh-tight); padding: .18rem .55rem;
278
+ border-radius: var(--hrl-radius-pill); background: var(--hrl-info-bg); color: var(--hrl-accent);
279
+ white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
186
280
  }
187
281
  .hrl-badge--ok { background: var(--hrl-ok-bg); color: var(--hrl-ok); }
188
282
  .hrl-badge--warn { background: var(--hrl-warn-bg); color: var(--hrl-warn); }
189
283
  .hrl-badge--bad { background: var(--hrl-bad-bg); color: var(--hrl-bad); }
190
- .hrl-badge--muted { background: var(--hrl-bg); color: var(--hrl-muted); }
191
- .hrl-mention { color: var(--hrl-accent); font-weight: 650; }
284
+ .hrl-badge--muted { background: var(--hrl-bg); color: var(--hrl-ink-2); }
285
+ /* Leave-type badge: ink text on grey; the HR-chosen colour (inline --hrl-type) is only the dot. */
286
+ .hrl-badge--type { background: var(--hrl-bg); color: var(--hrl-ink); }
287
+ .hrl-badge--type::before { content: ""; display: inline-block; width: 8px; height: 8px; margin-right: .3rem; border-radius: var(--hrl-radius-pill); background: var(--hrl-type); }
288
+ .hrl-mention { color: var(--hrl-accent); font-weight: 700; }
192
289
 
193
290
  /* Tables (stack to label/value rows on phones) */
194
291
  .hrl-table-wrap { overflow-x: auto; }
195
- .hrl-table { width: 100%; border-collapse: collapse; font-size: .88rem; background: var(--hrl-card); border-radius: var(--hrl-radius); }
292
+ .hrl-table { width: 100%; border-collapse: collapse; font-size: var(--hrl-t-small); background: var(--hrl-card); border-radius: var(--hrl-radius); }
196
293
  .hrl-table th, .hrl-table td { text-align: left; padding: .6rem .7rem; border-bottom: 1px solid var(--hrl-line); vertical-align: top; }
197
- .hrl-table th { color: var(--hrl-muted); font-size: .76rem; text-transform: uppercase; letter-spacing: .03em; }
294
+ .hrl-table th { color: var(--hrl-muted); font-size: var(--hrl-t-micro); text-transform: uppercase; letter-spacing: .03em; }
198
295
  .hrl-table td.hrl-num, .hrl-table th.hrl-num { text-align: right; font-variant-numeric: tabular-nums; }
199
296
  .hrl-table--tight th, .hrl-table--tight td { padding: .5rem .35rem; }
200
- @media (max-width: 640px) {
297
+ .hrl-table td[data-label=""] > :is(a, button) + :is(a, button, form) { margin-left: var(--hrl-s-2); }
298
+ .hrl-table td[data-label=""] > :is(a, button) + form { display: inline-block; vertical-align: top; } /* a button_to beside a link, not under it */
299
+ .hrl-table td[data-label=""] > form + form { margin-top: var(--hrl-s-2); }
300
+ .hrl-table tr:has(.hrl-btn) > td, .hrl-table tr:has(.hrl-btn) td > .hrl-badge { vertical-align: middle; } /* text lines up with a 44px button */
301
+ /* Stack on phones and on the narrow content column beside the 768-1023px rail. */
302
+ @media (max-width: 640px), (min-width: 768px) and (max-width: 1023px) {
201
303
  .hrl-table--stack thead { display: none; }
202
304
  .hrl-table--stack tr { display: block; border-bottom: 6px solid var(--hrl-bg); }
203
305
  .hrl-table--stack td { display: flex; flex-wrap: wrap; justify-content: space-between; gap: .8rem; border-bottom: 1px dashed var(--hrl-line); overflow-wrap: anywhere; }
204
- .hrl-table--stack td::before { content: attr(data-label); color: var(--hrl-muted); font-size: .78rem; }
306
+ .hrl-table--stack td::before { content: attr(data-label); color: var(--hrl-muted); font-size: var(--hrl-t-small); }
205
307
  .hrl-table--stack td.hrl-num { text-align: left; }
206
308
  .hrl-table--stack td.hrl-cell--void { display: none; }
309
+ .hrl-table--stack tr:last-child { border-bottom: 0; }
310
+ .hrl-table--stack td[data-label=""] { justify-content: flex-end; }
311
+ .hrl-table--stack td[data-label=""]::before { display: none; }
312
+ .hrl-table--stack td > .hrl-badge { align-self: center; } /* do not stretch beside a 44px button */
313
+ .hrl-table--stack td[data-label=""] > :is(a, button) + :is(a, button, form) { margin-left: 0; }
314
+ .hrl-table--stack td[data-label=""] > form + form { margin-top: 0; }
207
315
  }
208
316
 
209
317
  /* Forms */
210
318
  .hrl-field { margin-bottom: .8rem; }
211
- .hrl-field label { display: block; font-size: .82rem; font-weight: 600; margin-bottom: .3rem; }
212
- .hrl-field .hrl-hint { color: var(--hrl-muted); font-size: .76rem; margin-top: .25rem; }
319
+ .hrl-field label { display: block; font-size: var(--hrl-t-small); font-weight: 600; margin-bottom: .3rem; }
320
+ .hrl-hint { color: var(--hrl-muted); font-size: var(--hrl-t-small); margin-top: .25rem; }
213
321
  /* Full-width text-ish controls only — checkboxes/radios/hidden must keep
214
322
  their natural size (a bare [type] selector out-ranked .hrl-field--check
215
323
  and blew checkboxes up to 100% width). */
216
324
  .hrl-input,
217
325
  .hrl-field input[type]:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]),
218
326
  .hrl-field select, .hrl-field textarea {
219
- width: 100%; min-height: 44px; padding: .55rem .7rem; font: inherit;
220
- border: 1px solid var(--hrl-line); border-radius: var(--hrl-radius-sm);
327
+ width: 100%; min-height: var(--hrl-target); padding: .55rem .7rem; font: inherit; font-size: var(--hrl-t-body);
328
+ border: 1px solid var(--hrl-field); border-radius: var(--hrl-radius-sm);
221
329
  background: var(--hrl-card); color: var(--hrl-ink);
222
330
  }
223
331
  .hrl-field textarea { min-height: 90px; resize: vertical; }
224
- .hrl-field--check { display: flex; align-items: center; gap: .55rem; min-height: 44px; }
332
+ .hrl-filter { appearance: none; margin-bottom: var(--hrl-gap-target); } /* select_filter.js search box above its select */
333
+ .hrl-field--check { display: flex; align-items: center; gap: .55rem; min-height: var(--hrl-target); }
225
334
  .hrl-field--check input {
226
335
  width: 1.15rem; height: 1.15rem; min-height: 0; flex: 0 0 auto;
227
336
  accent-color: var(--hrl-accent);
228
337
  }
229
- .hrl-field--check label { margin-bottom: 0; font-weight: 600; }
230
- .hrl-errors { background: var(--hrl-bad-bg); color: var(--hrl-bad); border-radius: var(--hrl-radius-sm); padding: .7rem .9rem; margin-bottom: .85rem; font-size: .86rem; }
338
+ /* The 44px label is the tap target for the small box. */
339
+ .hrl-field--check label { margin-bottom: 0; font-weight: 600; flex: 1; min-height: var(--hrl-target); display: flex; align-items: center; }
340
+ .hrl-field--check label .hrl-hint { display: block; margin-top: 0; font-weight: 400; }
341
+ /* Rails wraps an invalid label/field in this div; contents keeps the stack, the re-pointed token reddens the border. */
342
+ .field_with_errors { display: contents; --hrl-field: var(--hrl-bad); }
343
+ .field_with_errors label { color: var(--hrl-bad); }
344
+ .hrl-errors { background: var(--hrl-bad-bg); color: var(--hrl-bad); border-radius: var(--hrl-radius-sm); padding: .7rem .9rem; margin-bottom: .85rem; font-size: var(--hrl-t-body); }
231
345
  .hrl-errors ul { margin: .3rem 0 0; padding-left: 1.1rem; }
232
346
  .hrl-form-actions { display: flex; flex-wrap: wrap; gap: .6rem; margin-top: 1rem; }
233
347
  .hrl-form-actions .hrl-btn { flex: 1; }
234
348
  @media (min-width: 700px) { .hrl-form-actions .hrl-btn { flex: 0 0 auto; min-width: 9rem; } }
235
349
 
236
350
  /* Radio chip row (kudos badges) */
237
- .hrl-chiprow { display: flex; flex-wrap: wrap; gap: .45rem; }
351
+ .hrl-chiprow { display: flex; flex-wrap: wrap; gap: var(--hrl-gap-target); }
238
352
  .hrl-chiprow label {
239
- border: 1px solid var(--hrl-line); border-radius: 999px; padding: .4rem .8rem;
240
- font-size: .8rem; font-weight: 600; cursor: pointer; min-height: 40px; display: inline-flex; align-items: center;
353
+ border: 1px solid var(--hrl-line); border-radius: var(--hrl-radius-pill); padding: .4rem .8rem;
354
+ font-size: var(--hrl-t-small); font-weight: 600; cursor: pointer; min-height: var(--hrl-target); display: inline-flex; align-items: center;
355
+ min-width: var(--hrl-target); justify-content: center;
241
356
  }
242
357
  .hrl-chiprow input { position: absolute; opacity: 0; pointer-events: none; }
243
358
  .hrl-chiprow input:checked + span, .hrl-chiprow label:has(input:checked) { background: var(--hrl-info-bg); border-color: var(--hrl-accent); color: var(--hrl-accent); }
359
+ /* The radio is invisible, so its focus ring goes on the chip. */
360
+ .hrl-chiprow label:has(input:focus-visible) { outline: var(--hrl-focus-w) solid var(--hrl-focus); outline-offset: var(--hrl-focus-offset); }
244
361
 
245
362
  /* Feed items */
363
+ .hrl-feed { list-style: none; margin: 0; padding: 0; }
246
364
  .hrl-feed__item { display: flex; gap: .7rem; padding: .8rem 0; border-bottom: 1px solid var(--hrl-line); }
247
365
  .hrl-feed__item:last-child { border-bottom: 0; }
248
366
  .hrl-avatar {
249
- flex: 0 0 auto; width: 38px; height: 38px; border-radius: 999px;
367
+ flex: 0 0 auto; width: 38px; height: 38px; border-radius: var(--hrl-radius-pill);
250
368
  background: var(--hrl-info-bg); color: var(--hrl-accent);
251
369
  display: inline-flex; align-items: center; justify-content: center; font-weight: 700;
252
370
  }
253
- .hrl-feed__meta { color: var(--hrl-muted); font-size: .76rem; }
371
+ .hrl-feed__meta { color: var(--hrl-muted); font-size: var(--hrl-t-small); }
254
372
  .hrl-feed__body { min-width: 0; flex: 1; overflow-wrap: anywhere; }
255
373
 
256
374
  /* Mention autocomplete menu */
257
375
  .hrl-mention-wrap { position: relative; }
258
376
  .hrl-mention-menu {
259
- position: absolute; left: 0; right: 0; z-index: 30;
377
+ position: absolute; left: 0; right: 0; z-index: var(--hrl-z-menu);
260
378
  background: var(--hrl-card); border: 1px solid var(--hrl-line);
261
379
  border-radius: var(--hrl-radius-sm); box-shadow: var(--hrl-shadow);
262
380
  max-height: 14rem; overflow-y: auto;
263
381
  }
264
382
  .hrl-mention-menu button {
265
383
  display: block; width: 100%; text-align: left; padding: .6rem .8rem;
266
- background: none; border: 0; font: inherit; cursor: pointer; min-height: 44px;
384
+ background: none; border: 0; font: inherit; cursor: pointer; min-height: var(--hrl-target);
267
385
  }
268
386
  .hrl-mention-menu button:hover, .hrl-mention-menu button.hrl-active { background: var(--hrl-bg); }
269
387
 
270
388
  /* Pagination */
271
- .hrl-pager { display: flex; align-items: center; justify-content: center; gap: .8rem; margin: 1rem 0; font-size: .88rem; }
389
+ .hrl-pager { display: flex; align-items: center; justify-content: center; gap: .8rem; margin: 1rem 0; font-size: var(--hrl-t-small); }
272
390
  .hrl-pager__info { color: var(--hrl-muted); }
273
391
 
274
392
  /* Org chart tree + reporting-line chain */
@@ -282,45 +400,65 @@
282
400
  }
283
401
  .hrl-tree__card--me { border-color: var(--hrl-accent); background: var(--hrl-info-bg); }
284
402
  .hrl-tree__name, .hrl-tree__role { min-width: 0; overflow-wrap: anywhere; }
285
- .hrl-tree__name { font-weight: 650; font-size: .9rem; }
286
- .hrl-tree__role { color: var(--hrl-muted); font-size: .8rem; }
403
+ .hrl-tree__name { font-weight: 700; font-size: var(--hrl-t-small); }
404
+ .hrl-tree__role { color: var(--hrl-muted); font-size: var(--hrl-t-small); }
287
405
  .hrl-chainrow { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
288
406
  .hrl-chainrow__step { display: inline-flex; align-items: center; gap: .4rem; min-width: 0; }
289
407
  .hrl-chainrow .hrl-badge { white-space: normal; overflow-wrap: anywhere; }
290
408
  .hrl-chainrow__arrow { color: var(--hrl-muted); }
291
409
 
292
410
  /* Definition list */
293
- .hrl-deflist { display: grid; grid-template-columns: minmax(7rem, auto) 1fr; gap: .35rem .9rem; font-size: .9rem; }
294
- .hrl-deflist dt { color: var(--hrl-muted); }
411
+ .hrl-deflist { display: grid; grid-template-columns: minmax(7rem, auto) minmax(0, 1fr); gap: .35rem .9rem; font-size: var(--hrl-t-body); }
412
+ .hrl-deflist dt { color: var(--hrl-muted); font-size: var(--hrl-t-small); }
295
413
  .hrl-deflist dd { margin: 0; overflow-wrap: anywhere; }
296
414
 
297
415
  /* Month grid (attendance) */
298
416
  /* minmax(0, …): a bare 1fr never shrinks below its nowrap tags and pushes Sa/Su off a phone */
299
- .hrl-mgrid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 3px; }
300
- .hrl-mgrid__head { text-align: center; color: var(--hrl-muted); font-size: .68rem; padding: .2rem 0; }
417
+ /* Phone: the grid alone reaches 4px from the card edge, with 2px gaps, so seven cells are 44px wide at 360px. */
418
+ .hrl-mgrid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 2px; }
419
+ .hrl-card > .hrl-mgrid { margin-inline: calc(var(--hrl-s-1) - .95rem); }
420
+ @media (min-width: 481px) {
421
+ .hrl-mgrid { gap: 3px; }
422
+ .hrl-card > .hrl-mgrid { margin-inline: 0; }
423
+ }
424
+ .hrl-mgrid__head { text-align: center; color: var(--hrl-muted); font-size: var(--hrl-t-micro); padding: .2rem 0; }
301
425
  .hrl-mgrid__day {
302
- min-height: 44px; border-radius: 7px; background: var(--hrl-card);
303
- border: 1px solid var(--hrl-line); padding: .2rem; font-size: .72rem;
426
+ min-height: var(--hrl-target); border-radius: var(--hrl-radius-sm); background: var(--hrl-card);
427
+ border: 1px solid var(--hrl-line); padding: .2rem calc(var(--hrl-s-1) / 2); font-size: var(--hrl-t-small);
304
428
  display: flex; flex-direction: column; gap: .1rem; position: relative;
305
429
  }
306
- .hrl-mgrid__day--muted { opacity: .45; }
307
- .hrl-mgrid__num { font-weight: 650; }
308
- .hrl-mgrid__tag { font-size: .6rem; color: var(--hrl-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
430
+ /* 2px side padding on phones leaves "Today" its 32px in a 39px cell at 320px. */
431
+ @media (min-width: 481px) { .hrl-mgrid__day { padding-inline: .2rem; } }
432
+ .hrl-mgrid__day--muted { color: var(--hrl-muted); }
433
+ .hrl-mgrid__num { font-weight: 600; }
434
+ .hrl-mgrid__tag { font-size: var(--hrl-t-micro); color: var(--hrl-ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
435
+ /* The day number's link covers its whole cell and keeps the cell's ink, which holds 4.5:1 on every tint. */
436
+ .hrl-mgrid__day a { color: inherit; }
437
+ .hrl-mgrid__day a::after { content: ""; position: absolute; inset: -1px; border-radius: var(--hrl-radius-sm); }
438
+ /* Legend swatch: a cell in badge form; the state modifiers below tint it. */
439
+ .hrl-mgrid__key { background: var(--hrl-card); border: 1px solid var(--hrl-line); color: var(--hrl-ink-2); }
309
440
  .hrl-mgrid__day--present { background: var(--hrl-ok-bg); border-color: transparent; }
310
441
  .hrl-mgrid__day--half { background: linear-gradient(135deg, var(--hrl-ok-bg) 50%, var(--hrl-card) 50%); }
311
442
  .hrl-mgrid__day--leave { background: var(--hrl-info-bg); border-color: transparent; }
443
+ .hrl-mgrid__day--half-leave { background: linear-gradient(135deg, var(--hrl-info-bg) 50%, var(--hrl-card) 50%); }
312
444
  .hrl-mgrid__day--holiday { background: var(--hrl-warn-bg); border-color: transparent; }
313
445
  .hrl-mgrid__day--weekend { background: var(--hrl-bg); }
314
446
  .hrl-mgrid__day--absent { background: var(--hrl-bad-bg); border-color: transparent; }
315
- .hrl-mgrid__flag { position: absolute; top: 3px; right: 4px; width: 7px; height: 7px; border-radius: 999px; background: var(--hrl-bad); }
447
+ .hrl-mgrid__day--today { box-shadow: inset 0 0 0 2px var(--hrl-accent); }
448
+ .hrl-mgrid__day--pending { border: 1px dashed var(--hrl-ink-2); }
449
+ .hrl-mgrid__flag { position: absolute; top: 3px; right: 4px; width: 7px; height: 7px; border-radius: var(--hrl-radius-pill); background: var(--hrl-bad); }
316
450
  .hrl-legend { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .6rem; }
451
+ .hrl-legend .hrl-mgrid__flag { position: static; display: inline-block; }
452
+ @media (min-width: 768px) { .hrl-mgrid--cal .hrl-mgrid__day { min-height: 88px; } }
317
453
 
318
454
  /* Utility */
319
455
  .hrl-muted { color: var(--hrl-muted); }
320
- .hrl-small { font-size: .8rem; }
456
+ .hrl-small { font-size: var(--hrl-t-small); }
321
457
  .hrl-mt { margin-top: .85rem; }
322
458
  .hrl-right { margin-left: auto; }
323
459
  .hrl-row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
460
+ .hrl-row--mb { margin-bottom: var(--hrl-s-3); }
461
+ .hrl-row--tabs > .hrl-btn { padding-inline: var(--hrl-s-2); } /* four approvals tabs fit one row beside the rail at 768px */
324
462
 
325
463
  /* Long-form policy text. Constrained measure — a policy read across the
326
464
  full width of a laptop is a policy nobody finishes. */
@@ -397,5 +535,5 @@
397
535
  }
398
536
 
399
537
  /* Notifications link, shown only when the host configures one. */
400
- .hrl-bell { display: inline-flex; align-items: center; gap: .4rem; min-height: 44px; font-size: .86rem; font-weight: 600; }
401
- .hrl-bell__count { min-width: 1.4rem; padding: 0 .4rem; border-radius: 9999px; background: var(--hrl-accent); color: var(--hrl-accent-ink); font-size: .75rem; text-align: center; }
538
+ .hrl-bell { display: inline-flex; align-items: center; gap: .4rem; min-height: var(--hrl-target); font-size: var(--hrl-t-small); font-weight: 600; }
539
+ .hrl-bell__count { min-width: 1.4rem; padding: 0 .4rem; border-radius: var(--hrl-radius-pill); background: var(--hrl-accent); color: var(--hrl-accent-ink); font-size: var(--hrl-t-micro); text-align: center; }
@@ -11,7 +11,7 @@ module HrLite
11
11
  end
12
12
 
13
13
  def new
14
- @asset = Asset.new
14
+ @asset = Asset.new(category: nil) # no chip preselected over the column default
15
15
  end
16
16
 
17
17
  def create
@@ -11,6 +11,36 @@ module HrLite
11
11
  @employees = @employees.select { |user| visible.include?(user.id) } if visible
12
12
  @records = AttendanceRecord.for_date(@date).where(user_id: @employees.map(&:id)).index_by(&:user_id)
13
13
  @flagged_count = @records.values.count(&:flagged?)
14
+ # "How many people punched where": off-site punches count as one place.
15
+ @offices = OfficeLocation.active.to_a
16
+ @places = %i[check_in check_out].index_with do |side|
17
+ @records.values.select { |r| r.public_send("#{side}_at") }.map do |r|
18
+ OfficeLocation.place(r.public_send("#{side}_lat"), r.public_send("#{side}_lng"), @offices)&.split(" · ")&.first || "No GPS"
19
+ end.tally
20
+ end
21
+ end
22
+
23
+ # Mon–Fri hours per person (Saturday never counts) against
24
+ # config.work_hours[:week], pro rata for holidays and full-day leave.
25
+ # The current week counts only the days already over.
26
+ def week
27
+ @monday = parse_date_param(params[:date]).beginning_of_week(:monday)
28
+ calendar = WorkingCalendar.new(@monday..(@monday + 4))
29
+ days = (@monday..[ @monday + 4, Date.current - 1 ].min).select { |d| calendar.working_day?(d) }
30
+ visible = hr_access.visible_user_ids("attendance.view")
31
+ employees = HrLite.active_employees(on: @monday)
32
+ employees = employees.select { |user| visible.include?(user.id) } if visible
33
+ records = AttendanceRecord.where(user_id: employees.map(&:id), date: days).group_by(&:user_id)
34
+ on_leave = days.index_with { |d| LeaveRequest.active_on(d).where(half_day: false).pluck(:user_id) }
35
+ week_hours = HrLite.config.work_hours&.dig(:week)
36
+
37
+ @rows = employees.map do |user|
38
+ worked = records.fetch(user.id, []).filter_map(&:worked_duration)
39
+ hours = worked.sum / 3600.0
40
+ target = week_hours && week_hours * days.count { |d| on_leave[d].exclude?(user.id) } / 5.0
41
+ { user: user, hours: hours, avg: worked.any? ? hours / worked.size : nil, target: target,
42
+ below: target.present? && target.positive? && hours < target }
43
+ end
14
44
  end
15
45
 
16
46
  # One employee's month + the regularization form for ?date=.
@@ -19,6 +49,7 @@ module HrLite
19
49
  @month = parse_month_param(params[:month])
20
50
  @day_status = DayStatus.new(user: @employee, range: @month.beginning_of_month..@month.end_of_month)
21
51
  @counts = @day_status.counts
52
+ @counts[:absent] -= 1 if @month.all_month.cover?(Date.current) && @day_status.for(Date.current).kind == :absent
22
53
  @edit_date = params[:date].present? ? parse_date_param(params[:date]) : nil
23
54
  @edit_record = @edit_date && AttendanceRecord.find_or_initialize_by(user_id: @employee.id, date: @edit_date)
24
55
  end
@@ -8,7 +8,7 @@ module HrLite
8
8
  # screen and each sees only their own rows.
9
9
  class ApprovalsController < ApplicationController
10
10
  def index
11
- @approvals = paginate(mine.includes(:step, :subject).order(:created_at))
11
+ @approvals = paginate(Approval.pending_for(hr_current_user).includes(:step, :subject).order(:created_at))
12
12
  @delegations = ApprovalDelegation.live_on(Date.current)
13
13
  .where(from_user_id: hr_current_user.id)
14
14
  .includes(:to_user)
@@ -16,16 +16,5 @@ module HrLite
16
16
  .where(to_user_id: hr_current_user.id)
17
17
  .includes(:from_user)
18
18
  end
19
-
20
- private
21
-
22
- # Rows addressed to this person, plus rows belonging to anybody who has
23
- # delegated to them while they are away.
24
- def mine
25
- standing_in_for = ApprovalDelegation.live_on(Date.current)
26
- .where(to_user_id: hr_current_user.id)
27
- .pluck(:from_user_id)
28
- Approval.pending.where(approver_id: [ hr_current_user.id, *standing_in_for ])
29
- end
30
19
  end
31
20
  end
@@ -8,6 +8,13 @@ module HrLite
8
8
  @record = AttendanceRecord.find_by(user_id: hr_current_user.id, date: Date.current)
9
9
  @day_status = DayStatus.new(user: hr_current_user, range: @month.beginning_of_month..@month.end_of_month)
10
10
  @counts = @day_status.counts
11
+ @counts[:absent] -= 1 if @month.all_month.cover?(Date.current) && @day_status.for(Date.current).kind == :absent
12
+ @pending_dates = RegularizationRequest.pending.where(user_id: hr_current_user.id, date: @month.all_month).pluck(:date).to_set
13
+ # ?day= opens the day detail; garbage (parsed as today), other months and the future open nothing.
14
+ day = parse_date_param(params[:day])
15
+ @day = day if params[:day] == day.iso8601 && @month.all_month.cover?(day) && day <= Date.current
16
+ @day_info = @day_status.for(@day) if @day
17
+ @day_ticket = RegularizationRequest.where(user_id: hr_current_user.id, date: @day).recent_first.first if @day
11
18
  end
12
19
 
13
20
  def check_in
@@ -4,6 +4,7 @@ module HrLite
4
4
  year = params[:year].to_i
5
5
  @year = year.between?(2000, 2100) ? year : Date.current.year
6
6
  @holidays = Holiday.where(date: Date.new(@year, 1, 1)..Date.new(@year, 12, 31)).order(:date)
7
+ @next_holiday_id = Holiday.company_wide.upcoming.pick(:id)
7
8
  end
8
9
  end
9
10
  end