Class: EmployeePhoneStatus

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable
Defined in:
app/models/employee_phone_status.rb

Overview

Model to keep track of a user's phone system settings and presence
== Schema Information

Table name: employee_phone_statuses
Database name: primary

id :integer not null, primary key
auto_away_after :time
click_to_call_integration :string(10) default("api")
date_set :datetime
dnd_alert_threshold_minutes :integer
extension :integer
last_alert :datetime
message :string
pbx_integration :integer default("none")
presence :string(20)
queue_statuses :jsonb
status_options :jsonb
sub_presence :string
created_at :datetime
updated_at :datetime
contact_point_id :integer
employee_id :integer
switchvox_account_id :integer

Indexes

by_pres_spres (presence,sub_presence)
employee_phone_statuses_employee_id_idx (employee_id)
idx_eps_contact_point_id (contact_point_id)
idx_eps_switchvox_account_id (switchvox_account_id)

Foreign Keys

fk_rails_... (contact_point_id => contact_points.id)
fk_rails_... (employee_id => parties.id) ON DELETE => cascade

Defined Under Namespace

Classes: StatusAlerter

Constant Summary

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#switchvox_account_idObject (readonly)



51
# File 'app/models/employee_phone_status.rb', line 51

validates :switchvox_account_id, presence: true, numericality: { greater_than: 0 }, if: :pbx_integration_switchvox?

Class Method Details

.broadcast_agents_status(employee_phone_statuses = nil, _options = {}) ⇒ Object

Retrieve the current database state of agent statuses and broadcast



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'app/models/employee_phone_status.rb', line 169

def self.broadcast_agents_status(employee_phone_statuses = nil, _options = {})
  employee_phone_statuses ||= EmployeePhoneStatus.all
  state_hsh = {}
  [employee_phone_statuses].flatten.each do |eps|
    state_hsh[eps.employee_id] = {
      presence: eps.presence,
      sub_presence: eps.sub_presence,
      message: eps.message,
      date_set: eps.date_set,
      timestamp: (eps.date_set.to_f * 1000).to_i # Browser timestamps are in ms since epoch (not seconds)
    }
  end
  # Todo Replace with SSE
  # ActionCable.server.broadcast('agent_statuses', state_hsh) if state_hsh.present?
end

.dnd_alertsActiveRecord::Relation<EmployeePhoneStatus>

A relation of EmployeePhoneStatuses that are dnd alerts. Active Record Scope

Returns:

See Also:



65
66
67
68
69
# File 'app/models/employee_phone_status.rb', line 65

scope :dnd_alerts, lambda {
  where.not(dnd_alert_threshold_minutes: nil, date_set: nil)
       .where(presence: 'dnd')
       .where("date_set + dnd_alert_threshold_minutes * INTERVAL '1 minutes' > ?", Time.current)
}

.extension_to_employee_id_hashObject



143
144
145
146
147
148
149
# File 'app/models/employee_phone_status.rb', line 143

def self.extension_to_employee_id_hash
  Rails.cache.fetch(:pbx_extension_to_employee_id_index, expires_in: 1.day) do
    Employee.active_employees.includes(:employee_phone_status, :contact_points)
            .reject { |emp| emp.pbx_extension.nil? }
            .each_with_object({}) { |emp, hsh| hsh[emp.pbx_extension] = emp.id }
  end
end

.garnish_employees(employees = nil, only_with_phone_record = false) ⇒ Object

Takes an employee relation and filter on it and associate all records
for performance in phone operation



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'app/models/employee_phone_status.rb', line 79

def self.garnish_employees(employees = nil, only_with_phone_record = false)
  # Handle single id
  employees = [employees] if employees.is_a? Integer

  # Handle array of ids
  employees = Employee.where(id: employees) if employees &&
                                               employees.try(:[], 0).try(:is_a?, Integer)

  # Defaults to all
  employees ||= Employee.all
  # Includes common records and filter by active and phone enabled employees
  employees = employees.active_employees.phone_enabled.joins(:employee_record).includes(:employee_record, :employee_phone_status)
  # Enforce presence of employee phone status if only with phone record option specified
  employees = employees.joins(:employee_phone_status) if only_with_phone_record
  employees
end

.initialize_and_push_presence(employees, presence, sub_presence = nil, options = {}) ⇒ Object

This method will set the status on one or multiple employees then sync statuses to the phone system



125
126
127
128
129
130
131
# File 'app/models/employee_phone_status.rb', line 125

def self.initialize_and_push_presence(employees, presence, sub_presence = nil, options = {})
  garnish_employees(employees, false).flatten.each do |employee|
    # Find out the status id we should be using
    eps = employee.employee_phone_status || employee.build_employee_phone_status
    eps.push_presence presence, sub_presence, options
  end
end

.migrate_extensionsObject



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'app/models/employee_phone_status.rb', line 96

def self.migrate_extensions
  Employee.all.each do |employee|
    next unless cp = employee.contact_points.where("detail ILIKE '+1847550%'").first

    ext = "8#{cp.detail.last(2)}"
    puts "Employee id #{employee.id} #{employee.full_name} Ext: #{ext}"
    if eps = employee.employee_phone_status
      eps.update_attribute(:extension, ext)
    end
    # Find 800 #
    if cp800 = employee.contact_points.where("detail ILIKE '+18008755285'").first
      cp800.detail = cp800.detail + " x#{ext}"
      cp800.save
    end
  end
end

.pull_presence(employees = nil, _options = {}) ⇒ Object

Pull all current presence statuses from the phone system



134
135
136
137
138
139
140
141
# File 'app/models/employee_phone_status.rb', line 134

def self.pull_presence(employees = nil, _options = {})
  result = {}
  garnish_employees(employees).each do |employee|
    employee_phone_status = employee.employee_phone_status || employee.build_employee_phone_status
    result[employee.id] = employee_phone_status.pull_presence
  end
  result
end

.pull_queue_status(_options = {}) ⇒ Object

Retrieves switchvox queue statuses and record them in our database
You can force a full pull rather than targetted to only queues
retrieved previously by passing ignore_queue_filter: true to the option hash



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'app/models/employee_phone_status.rb', line 188

def self.pull_queue_status(_options = {})
  queue_entries = []

  # Hash keys are the employee switchvox account id
  queue_status_hsh = Phone::Pbx.instance.get_queues_status

  updated_employee_phone_statuses_ids = []
  # Loop through all accounts and queue statuses
  queue_status_hsh.each do |, queue_statuses|
    next unless employee_phone_status = EmployeePhoneStatus.where(switchvox_account_id: ).first

    # Store the raw results from the api
    employee_phone_status.queue_statuses = queue_statuses
    # Save which will store a new version if in queue changed
    employee_phone_status.save
    # Mark this eps as good
    updated_employee_phone_statuses_ids << employee_phone_status.id
  end
  # Now any employee phone status without an entry get cleared
  invalid_eps = EmployeePhoneStatus.all
  invalid_eps = invalid_eps.where.not(id: updated_employee_phone_statuses_ids) if updated_employee_phone_statuses_ids.present?
  invalid_eps.update_all(queue_statuses: {})

  queue_entries
end

.push_presence(employees = nil, _options = {}) ⇒ Object

Pushes the presence of an employee to the phone system



114
115
116
117
118
119
120
121
122
# File 'app/models/employee_phone_status.rb', line 114

def self.push_presence(employees = nil, _options = {})
  employees = garnish_employees(employees, true)
  employees.each do |employee|
    employee.employee_phone_status.push_presence
  end
  # Retrieve call status

  broadcast_agents_status employees.map(&:employee_phone_status)
end

.refresh_all_status_optionsObject



320
321
322
# File 'app/models/employee_phone_status.rb', line 320

def self.refresh_all_status_options
  all.each(&:refresh_status_options)
end

Instance Method Details

#auto_away_time_checkObject

Checks if the user should be logged out, if so saves the presence away automatically
Then return true, otherwise returns false



290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/employee_phone_status.rb', line 290

def auto_away_time_check
  if time_to_set_away? and presence != 'away' and !current_status_set_after_auto_away?
    logger.info "[EmployeePhoneStatus:auto_away_time_check] Time check on employee id #{employee_id}, auto away at #{auto_away_after}, current presence: #{presence}. Changing status to away"
    self.date_set = Time.current
    self.presence = 'away'
    self.sub_presence = nil
    save
    true
  else
    logger.info "[EmployeePhoneStatus:auto_away_time_check] Time check on employee id #{employee_id}, auto away at #{auto_away_after}, current status #{presence} set at #{date_set}. Skipping"
    false
  end
end

#broadcast_agent_statusObject



151
152
153
# File 'app/models/employee_phone_status.rb', line 151

def broadcast_agent_status
  self.class.broadcast_agents_status(self)
end

#contact_pointContactPoint



45
# File 'app/models/employee_phone_status.rb', line 45

belongs_to :contact_point, optional: true

#curated_presence_listObject

Retrives a list of status options, business rules dictate we only care
about your available options, there's only one away and one dnd



352
353
354
355
356
357
358
359
360
361
362
# File 'app/models/employee_phone_status.rb', line 352

def curated_presence_list
  # Retrieve 'available' statuses followed by away and dnd status
  valid_presence_list.map do |opt|
    {
      active: (opt['active'] == '1'),
      id: opt['id'].to_i,
      sub_presence: opt['sub_presence'].presence,
      presence: opt['presence']
    }
  end
end

#current_status_set_after_auto_away?Boolean

Returns:

  • (Boolean)


284
285
286
# File 'app/models/employee_phone_status.rb', line 284

def current_status_set_after_auto_away?
  date_set.today? && date_set.to_time_of_day > auto_away_after
end

#dnd_alert_threshold_secondsObject



270
271
272
# File 'app/models/employee_phone_status.rb', line 270

def dnd_alert_threshold_seconds
  dnd_alert_threshold_minutes * 60 if dnd_alert_threshold_minutes
end

#dnd_presence_alert?Boolean

Returns:

  • (Boolean)


274
275
276
# File 'app/models/employee_phone_status.rb', line 274

def dnd_presence_alert?
  minutes_in_current_status && dnd_alert_threshold_minutes && minutes_in_current_status >= dnd_alert_threshold_minutes
end

#employeeEmployee

Returns:

See Also:



44
# File 'app/models/employee_phone_status.rb', line 44

belongs_to :employee, inverse_of: :employee_phone_status

#employee_phone_status_changesActiveRecord::Relation<EmployeePhoneStatusChange>

Returns:

See Also:



47
# File 'app/models/employee_phone_status.rb', line 47

has_many :employee_phone_status_changes

#enqueue_crm_navbar_presence_refreshObject

Targeted (single-user) refresh of the navbar presence dot. Only enqueues
when something the dot reflects actually changed (presence, sub_presence,
or message). Coalescing happens inside CrmNavbarRefreshWorker.



158
159
160
161
162
163
164
165
166
# File 'app/models/employee_phone_status.rb', line 158

def enqueue_crm_navbar_presence_refresh
  return if employee_id.blank?
  return unless previously_new_record? ||
                saved_change_to_presence? ||
                saved_change_to_sub_presence? ||
                saved_change_to_message?

  CrmNavbarRefreshWorker.schedule(user_id: employee_id, badge: :presence_dot)
end

#logged_in_queue?Boolean

Is the user logged in queue? we detect in the last retrieved queue status
if user is logged in all queue. It's an all or nothing, logged in all return true
otherwise false

Returns:

  • (Boolean)


316
317
318
# File 'app/models/employee_phone_status.rb', line 316

def logged_in_queue?
  (queue_statuses || {}).values.all? { |v| v['logged_in_status'] == 'logged_in' } || false
end

#minutes_in_current_statusObject



264
265
266
267
268
# File 'app/models/employee_phone_status.rb', line 264

def minutes_in_current_status
  return unless date_set

  ((Time.current - date_set) / 60).ceil
end

#pull_presence(_force = false) ⇒ Object

Pulls the pbx presence and update locally



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'app/models/employee_phone_status.rb', line 243

def pull_presence(_force = false)
  return true if auto_away_time_check # Skip the API, just go away

  if status_info = Phone::Pbx.instance.get_presence_status()
    self.presence = status_info[:presence].presence
    self.sub_presence = status_info[:sub_presence].presence
    logger.debug("[EmployeePhoneStatus:pull_presence] pulled", employee_phone_status_id: id, switchvox_account_id: )
    return :no_status_change unless presence_changed? or sub_presence_changed?

    self.message = status_info[:message].presence
    self.date_set = status_info[:date_set].presence
    return :status_changed if save

    :error_saving_record

  else
    logger.error "[EmployeePhoneStatus:pull_presence:#{id}] no presence status could be retrieved for switchvox_account_id #{}"
    :api_call_failure
  end
end

#push_presence(new_presence = nil, new_sub_presence = nil, options = {}) ⇒ Object

Pushes the local presence settings to the pbx api



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'app/models/employee_phone_status.rb', line 215

def push_presence(new_presence = nil, new_sub_presence = nil, options = {})
  # First save new presence if specified
  if new_presence
    self.presence = new_presence
    self.sub_presence = new_sub_presence
    self.date_set = Time.current
  else
    auto_away_time_check
  end
  return false unless valid?

  status_id = status_id_for_presence
  if status_id
    logger.info "[EmployeePhoneStatus(#{id})::push_presence] Setting presence id #{status_id} -> presence: #{presence}, sub_presence: #{sub_presence}, queue: #{should_be_in_queue?}, employee: #{employee_id}"
    success = Phone::Pbx.instance.update_unified_presence(account_id: ,
                                                          status_id: status_id,
                                                          log_in_queue: should_be_in_queue?,
                                                          call_queue_account_ids: queue_statuses.try(:keys) || [])
    if success
      save # Only when api call is successful do we save the record
      self.class.broadcast_agents_status(self) if options[:broadcast]
    end
  else
    logger.error "[EmployeePhoneStatus(#{id})::push_presence] is unable to find a status_id to match to, status id is #{status_id}, "
  end
end

#refresh_status_optionsObject

Update presence status list



325
326
327
328
329
330
331
# File 'app/models/employee_phone_status.rb', line 325

def refresh_status_options
  return unless  &&  > 0

  option_list = Phone::Pbx.instance.get_presence_options_list 
  update_attribute :status_options, option_list
  option_list
end

#should_be_in_queue?Boolean

Should be in queue ?

Returns:

  • (Boolean)


309
310
311
# File 'app/models/employee_phone_status.rb', line 309

def should_be_in_queue?
  presence == 'available' && sub_presence != 'Non Queue'
end

#status_id_for_presenceObject

Retrieve the presence status id for a given presence and sub status
Using the cached status_options previously stored by refresh_status_options
WIll attempt to retrieve status options if attribute is blank



336
337
338
339
# File 'app/models/employee_phone_status.rb', line 336

def status_id_for_presence
  refresh_status_options unless status_options.present?
  curated_presence_list.detect { |so| so[:presence].presence == presence.to_s.presence && so[:sub_presence].presence == sub_presence.to_s.presence }.try(:[], :id)
end

#sub_presence_optionsObject



304
305
306
# File 'app/models/employee_phone_status.rb', line 304

def sub_presence_options
  curated_presence_list.map { |s| s[:sub_presence] }.compact
end

#time_to_set_away?Boolean

Checks if it's time to go on auto away

Returns:

  • (Boolean)


279
280
281
282
# File 'app/models/employee_phone_status.rb', line 279

def time_to_set_away?
  tod_time_now = Time.current.to_time_of_day
  auto_away_after and tod_time_now >= auto_away_after
end

#valid_presence_listObject

Filters out status options



342
343
344
345
346
347
348
# File 'app/models/employee_phone_status.rb', line 342

def valid_presence_list
  status_options.select do |so|
    so['presence'] == 'available' ||
      (so['presence'] == 'away' && so['sub_presence'].blank?) ||
      so['presence'] == 'dnd'
  end
end