Module: Crm::TimeOffsHelper

Includes:
TimeOffPlannedWorkDayHelper
Defined in:
app/helpers/crm/time_offs_helper.rb

Constant Summary collapse

MANAGER_DEPARTMENT_ICON_BY_NAME =

Canonical department label (lowercase) -> Font Awesome icon name for fa_icon (sharp regular).
Used for exact match first, then substring scan (see MANAGER_DEPARTMENT_ICON_FRAGMENTS).
Includes administration / admin / no department; add new departments here only.

{
  'it' => 'building',
  'information technology' => 'building',
  'engineering' => 'screwdriver-wrench',
  'support' => 'headset',
  'customer support' => 'headset',
  'sales' => 'handshake',
  'marketing' => 'bullhorn',
  'finance' => 'coins',
  'accounting' => 'file-invoice-dollar',
  'hr' => 'people-group',
  'human resources' => 'people-group',
  'operations' => 'gears',
  'warehouse' => 'warehouse',
  'shipping' => 'truck',
  'logistics' => 'truck-fast',
  'legal' => 'scale-balanced',
  'executive' => 'briefcase',
  'management' => 'user-tie',
  'tech 24x7' => 'clock',
  'production' => 'industry',
  'quality' => 'clipboard-check',
  'purchasing' => 'cart-shopping',
  'buying' => 'cart-shopping',
  'administration' => 'shield-halved',
  'admin' => 'shield-halved',
  'no department' => 'users'
}.freeze
MANAGER_DEPARTMENT_ICON_FRAGMENT_EXCLUDE_KEYS =

Keys excluded from substring matching: +include?+ would false-positive (e.g. "digital" for "it").
Those labels are still handled by MANAGER_DEPARTMENT_ICON_BY_NAME (exact) or /\bit\b/ / /\bhr\b/ below.

%w[it hr].freeze
MANAGER_DEPARTMENT_ICON_FRAGMENTS =

Derived from MANAGER_DEPARTMENT_ICON_BY_NAME: longest keys first so "customer support" beats "support".

MANAGER_DEPARTMENT_ICON_BY_NAME
.reject { |k, _| MANAGER_DEPARTMENT_ICON_FRAGMENT_EXCLUDE_KEYS.include?(k) }
.sort_by { |k, _| [-k.length, k] }
.freeze
MANAGER_DEPARTMENT_ICON_FALLBACKS =
%w[
  building
  briefcase
  users
  industry
  star
  diagram-project
].freeze
PENDING_BADGE_VARIANT_CLASSES =

Bootstrap background utilities for +pending_badge+ (+variant+ must be a known key).

{
  primary: 'bg-primary',
  warning: 'bg-warning text-dark'
}.freeze
TIME_OFF_TYPE_DISPLAY_TEXT_CLASS =

Maps time-off row type (TimeOffType#name or the literal "holiday") to Bootstrap 5 text utilities.
Output is always from a fixed allowlist — never interpolates DB color strings into markup.

{
  'holiday' => 'text-warning',
  'Vacation' => 'text-success',
  'Birthday' => 'text-warning',
  'Unpaid leave' => 'text-secondary',
  'Community Service' => 'text-success',
  'Short-Term Disability (STD)' => 'text-primary',
  'Jury Duty' => 'text-info',
  'Bereavement' => 'text-secondary',
  'Banked Time' => 'text-primary'
}.freeze
FALLBACK_TEXT_CLASSES =
%w[
  text-primary text-secondary text-success text-danger text-warning text-info text-body-secondary
].freeze
TIME_OFF_TYPE_UNIT_OPTIONS =
[['Hours', 'hour'], ['Days', 'day']].freeze
TIME_OFF_TYPE_ICON_SUGGESTIONS =

Kebab-case Font Awesome icon names (no fa- prefix) for Tom Select suggestions.

%w[
  umbrella-beach calendar-days calendar-xmark calendar-check sun snowflake
  heart-pulse bandage person-falling plane car gift baby
  cross house-chimney clock mug-hot briefcase beach-ball
  ballot-check star sparkles martini-glass cake-candles hands-praying
  user-doctor stethoscope mask-face thermometer
].freeze
TIME_OFF_TYPE_COLOR_PICKER_DEFAULT =

Default swatch for <input type="color"> when none is set or value is not a hex the input accepts.

'#913120'
TIME_OFF_POLICY_ACCRUAL_FREQUENCY_OPTIONS =

Values match existing policy rows (e.g. seeds) and Employee#accrual handling uses .downcase.

%w[Weekly Monthly Yearly].map { |w| [w, w] }.freeze
TIME_OFF_POLICY_WEEKLY_DAY_OPTIONS =
Date::DAYNAMES.each_with_index.map { |day, index| [day, index] }.freeze
TIME_OFF_POLICY_ANNUAL_ACCRUAL_TIMING_OPTIONS =
[
  ['Beginning of Year', 'beginning_of_year'],
  ['End of Year', 'end_of_year']
].freeze

Instance Method Summary collapse

Methods included from TimeOffPlannedWorkDayHelper

#time_off_planned_work_day_summary

Instance Method Details

#manager_department_icon(department_label) ⇒ Object

Icon for a manager dashboard department heading (decorative; label remains plain text).



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'app/helpers/crm/time_offs_helper.rb', line 59

def manager_department_icon(department_label)
  key = department_label.to_s.strip.downcase
  return 'users' if key.blank?

  icon = MANAGER_DEPARTMENT_ICON_BY_NAME[key]
  return icon if icon

  MANAGER_DEPARTMENT_ICON_FRAGMENTS.each do |fragment, frag_icon|
    return frag_icon if key.include?(fragment)
  end

  return 'building' if /\bit\b/.match?(key)
  return 'people-group' if /\bhr\b/.match?(key)

  idx = key.bytes.sum % MANAGER_DEPARTMENT_ICON_FALLBACKS.size
  MANAGER_DEPARTMENT_ICON_FALLBACKS[idx]
end

#manager_schedule_grid_cell_kind(row_data, slot, slot_end) ⇒ Object

Row hash from Crm::TimeOffsController#setup_manager_dashboard_data (+ :time_off_request_date).



232
233
234
235
236
237
238
239
240
# File 'app/helpers/crm/time_offs_helper.rb', line 232

def manager_schedule_grid_cell_kind(row_data, slot, slot_end)
  Crm::ManagerScheduleGridSlot.cell_kind(
    slot: slot,
    slot_end: slot_end,
    schedule_day: row_data[:schedule_day],
    time_off_request_date: row_data[:time_off_request_date],
    ongoing_time_off_request: row_data[:ongoing_request]
  )
end

#pending_badge(count, variant: :primary, title: '', extra_classes: '') ⇒ Object

Pill badge for manager employee chip pending counts; returns nil when +count+ is not positive.



84
85
86
87
88
89
90
91
92
93
# File 'app/helpers/crm/time_offs_helper.rb', line 84

def pending_badge(count, variant: :primary, title: '', extra_classes: '')
  return nil unless count.respond_to?(:positive?) && count.positive?

  variant_classes = PENDING_BADGE_VARIANT_CLASSES.fetch(variant.to_sym)
  class_parts = ['badge', 'rounded-pill', variant_classes]
  class_parts.concat(extra_classes.to_s.split) if extra_classes.present?
  attrs = { class: class_parts.join(' ') }
  attrs[:title] = title if title.present?
  tag.span(count.to_s, **attrs)
end

#time_off_policy_parent_options(record) ⇒ Object



225
226
227
228
229
# File 'app/helpers/crm/time_offs_helper.rb', line 225

def time_off_policy_parent_options(record)
  scope = TimeOffPolicy.order(:name)
  scope = scope.where.not(id: record.id) if record.persisted?
  scope.pluck(:name, :id)
end

#time_off_type_color_class(type) ⇒ Object



113
114
115
116
117
118
119
120
# File 'app/helpers/crm/time_offs_helper.rb', line 113

def time_off_type_color_class(type)
  key = type.to_s
  return TIME_OFF_TYPE_DISPLAY_TEXT_CLASS[key] if TIME_OFF_TYPE_DISPLAY_TEXT_CLASS.key?(key)

  # Unknown type names (e.g. newly added in admin): pick a stable class from the allowlist only.
  idx = key.bytes.sum % FALLBACK_TEXT_CLASSES.size
  FALLBACK_TEXT_CLASSES[idx]
end

#time_off_type_color_picker_value(record) ⇒ Object

HTML color input expects #rrggbb; fall back when DB has named colors or invalid values.



188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'app/helpers/crm/time_offs_helper.rb', line 188

def time_off_type_color_picker_value(record)
  c = record.color.to_s.strip
  return TIME_OFF_TYPE_COLOR_PICKER_DEFAULT if c.blank?

  if c.match?(/\A#[0-9a-fA-F]{6}\z/i)
    c
  elsif c.match?(/\A#[0-9a-fA-F]{3}\z/i)
    "##{c[1]}#{c[1]}#{c[2]}#{c[2]}#{c[3]}#{c[3]}"
  elsif c.match?(/\A#[0-9a-fA-F]{8}\z/i)
    c[0..6]
  else
    TIME_OFF_TYPE_COLOR_PICKER_DEFAULT
  end
end

#time_off_type_icon_tom_select_collection(record) ⇒ Object

Pairs [label, value] for icon Tom Select; includes current value if not in the suggestion list.



204
205
206
207
208
209
210
211
212
# File 'app/helpers/crm/time_offs_helper.rb', line 204

def time_off_type_icon_tom_select_collection(record)
  normalized = record.icon.to_s.strip.sub(/\Afa-/, '').tr('_', '-').downcase
  pairs = TIME_OFF_TYPE_ICON_SUGGESTIONS.map { |i| [i.tr('-', ' ').titleize, i] }
  if normalized.present? && TIME_OFF_TYPE_ICON_SUGGESTIONS.exclude?(normalized)
    pairs = pairs.dup
    pairs.unshift(["#{normalized.tr('-', ' ').titleize} (current)", normalized])
  end
  pairs
end

#vacation_summary_bar_chart_data(summary) ⇒ Object

Series for the Summary tab bar chart (horizontal bars, same metrics as the KPI cards).



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'app/helpers/crm/time_offs_helper.rb', line 132

def vacation_summary_bar_chart_data(summary)
  return if summary.blank?

  rows = [
    {
      label: 'Used time as of today',
      value: summary[:used_until_today].to_f,
      icon: 'fa-regular fa-clock',
      fill_class: 'bg-danger'
    },
    {
      label: 'Available time as of today',
      value: summary[:available_today].to_f,
      icon: 'fa-regular fa-circle-check',
      fill_class: 'bg-success'
    },
    {
      label: 'Approved planned until year-end',
      value: summary[:approved_between_today_and_eoy].to_f,
      icon: 'fa-regular fa-calendar-check',
      fill_class: 'bg-warning'
    },
    {
      label: 'Pending approval until year-end',
      value: summary[:pending_between_today_and_eoy].to_f,
      icon: 'fa-regular fa-hourglass-half',
      fill_class: 'bg-secondary'
    },
    {
      label: 'Available at year-end',
      value: summary[:available_eoy].to_f,
      icon: 'fa-regular fa-calendar-xmark',
      fill_class: 'bg-primary'
    }
  ]
  max_value = rows.map { |r| r[:value] }.max.to_f
  max_value = 1.0 if max_value <= 0

  { rows:, max_value: }
end

#vacation_summary_hours_display(value) ⇒ Object

Matches TimeOffBalanceCalculator#format_number for vacation_summary raw values.



123
124
125
126
127
128
129
# File 'app/helpers/crm/time_offs_helper.rb', line 123

def vacation_summary_hours_display(value)
  return 0 if value.blank?

  n = value.to_f
  rounded = n.round(2)
  (rounded % 1).zero? ? rounded.to_i : rounded
end