Module: Crm::TimeOffsHelper

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

Overview

View helper: time offs.

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 =

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 =

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 =

Available time off type unit options.

[%w[Hours hour], %w[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 =

Available time off policy weekly day options.

Date::DAYNAMES.each_with_index.map { |day, index| [day, index] }.freeze
TIME_OFF_POLICY_ANNUAL_ACCRUAL_TIMING_OPTIONS =

Available 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_planned_hours, #time_off_planned_work_day_summary, #time_off_planned_work_day_usual_summary

Instance Method Details

#manager_department_icon(department_label) ⇒ Object

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



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

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).



239
240
241
242
243
244
245
246
247
# File 'app/helpers/crm/time_offs_helper.rb', line 239

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.



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

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



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

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

#time_off_type_color_class(type) ⇒ Object



116
117
118
119
120
121
122
123
# File 'app/helpers/crm/time_offs_helper.rb', line 116

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.



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'app/helpers/crm/time_offs_helper.rb', line 192

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

  case c
  when /\A#[0-9a-fA-F]{6}\z/i
    c
  when /\A#[0-9a-fA-F]{3}\z/i
    "##{c[1]}#{c[1]}#{c[2]}#{c[2]}#{c[3]}#{c[3]}"
  when /\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.



209
210
211
212
213
214
215
216
217
# File 'app/helpers/crm/time_offs_helper.rb', line 209

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).



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
172
173
174
# File 'app/helpers/crm/time_offs_helper.rb', line 135

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: 'clock',
      fill_class: 'bg-danger'
    },
    {
      label: 'Available time as of today',
      value: summary[:available_today].to_f,
      icon: 'circle-check',
      fill_class: 'bg-success'
    },
    {
      label: 'Approved planned until year-end',
      value: summary[:approved_between_today_and_eoy].to_f,
      icon: 'calendar-check',
      fill_class: 'bg-warning'
    },
    {
      label: 'Pending approval until year-end',
      value: summary[:pending_between_today_and_eoy].to_f,
      icon: 'hourglass-half',
      fill_class: 'bg-secondary'
    },
    {
      label: 'Available at year-end',
      value: summary[:available_eoy].to_f,
      icon: 'calendar-xmark',
      fill_class: 'bg-primary'
    }
  ]
  max_value = rows.pluck(: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.



126
127
128
129
130
131
132
# File 'app/helpers/crm/time_offs_helper.rb', line 126

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