Class: EmployeePhoneStatus
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- EmployeePhoneStatus
- 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 :enum default("api")
date_set :datetime
dnd_alert_threshold_minutes :integer
extension :integer
last_alert :datetime
message :string
pbx_integration :enum 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: BroadcastSwitchboard, QueueCoverageImpact, QueueStatusPuller, StatusAlerter
Constant Summary collapse
- QUEUE_LOGIN_STATES =
Queue login states.
{ 'logged_in' => true, 'permanent' => true, 'logged_out' => false }.freeze
Constants included from Models::Auditable
Models::Auditable::ALWAYS_IGNORED
Constants included from Models::Schedulable
Models::Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
-
#switchvox_account_id ⇒ Object
readonly
Validates switchvox account id, pbx integration switchvox.
Belongs to collapse
-
#contact_point ⇒ ContactPoint?
The contact point this record belongs to.
-
#employee ⇒ Employee
The employee this record belongs to.
Methods included from Models::Auditable
Has many collapse
-
#employee_phone_status_changes ⇒ ActiveRecord::Relation<EmployeePhoneStatusChange>
The associated employee phone status changes.
Class Method Summary collapse
-
.broadcast_agents_status(employee_phone_statuses = nil, _options = {}) ⇒ Object
Retrieve the current database state of agent statuses and broadcast.
-
.dnd_alerts ⇒ ActiveRecord::Relation<EmployeePhoneStatus>
A relation of EmployeePhoneStatuses that are dnd alerts.
-
.extension_to_employee_id_hash ⇒ Object
Extension to employee id hash.
-
.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.
-
.initialize_and_push_presence(employees, presence, sub_presence = nil, options = {}) ⇒ void
This method will set the status on one or multiple employees then sync statuses to the phone system.
-
.migrate_extensions ⇒ Object
Migrate extensions.
-
.pull_presence(employees = nil, _options = {}) ⇒ Object
Pull all current presence statuses from the phone system.
-
.pull_queue_status(queue_status_hsh = nil, observed_at: nil) ⇒ 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.
-
.push_presence(employees = nil, _options = {}) ⇒ Object
Pushes the presence of an employee to the phone system.
-
.refresh_all_status_options ⇒ Object
Refresh all status options.
-
.switchboard_stream_name(employee_id, can_manage:) ⇒ String
Builds the internal Action Cable stream for one watched employee and permission tier.
Instance Method Summary collapse
-
#auto_away_time_check ⇒ Object
Applies an automatic away transition in memory.
-
#broadcast_agent_status ⇒ Object
Broadcast agent status.
-
#broadcast_switchboard_card ⇒ void
Delegates switchboard card updates after relevant commits.
-
#curated_presence_list ⇒ Object
Retrives a list of status options, business rules dictate we only care about your available options, there's only one away and one dnd.
-
#current_status_set_after_auto_away? ⇒ Boolean
Whether the record current status set after auto away.
-
#dnd_alert_threshold_seconds ⇒ Object
Dnd alert threshold seconds.
-
#dnd_presence_alert? ⇒ Boolean
Whether the record dnd presence alert.
-
#enqueue_crm_navbar_presence_refresh ⇒ Object
Targeted (single-user) refresh of the navbar presence dot.
-
#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.
-
#minutes_in_current_status ⇒ Object
Minutes in current status.
-
#pull_presence(_force = false) ⇒ Object
Pulls the pbx presence and update locally.
-
#push_presence(new_presence = nil, new_sub_presence = nil, options = {}) ⇒ Boolean
Pushes the local presence settings to the pbx api.
-
#queue_ids_requiring_status(log_in_queue:) ⇒ Array<String>
Limits queue API writes to states that differ from the desired presence.
-
#queue_logged_in?(queue_id) ⇒ Boolean
Returns whether the latest observed PBX state has this queue actively routing calls to the employee.
-
#queue_login_status(queue_id) ⇒ String?
Normalizes the latest observed PBX queue state.
-
#refresh_status_options ⇒ Object
Update presence status list.
-
#refresh_switchboard ⇒ void
Delegates refresh when this employee enters the switchboard roster.
-
#remove_switchboard_card ⇒ void
Delegates removal when this employee leaves the switchboard roster.
-
#replace_switchboard_card ⇒ void
Delegates an unconditional card replacement for related visible changes.
-
#should_be_in_queue?(presence: self.presence, sub_presence: self.sub_presence) ⇒ Boolean
Checks whether a presence accepts phone-queue calls.
-
#status_id_for_presence ⇒ Object
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.
-
#sub_presence_options ⇒ Object
Sub presence options.
-
#time_to_set_away? ⇒ Boolean
Checks if it's time to go on auto away.
-
#valid_presence_list ⇒ Object
Filters out status options.
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::Schedulable
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Instance Attribute Details
#switchvox_account_id ⇒ Object (readonly)
Validates switchvox account id, pbx integration switchvox.
Validations (if => #pbx_integration_switchvox? ):
- Presence
- Numericality ({ greater_than: 0 })
Validations (if => #switchvox_account_id ):
61 |
# File 'app/models/employee_phone_status.rb', line 61 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
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 |
# File 'app/models/employee_phone_status.rb', line 249 def self.broadcast_agents_status(employee_phone_statuses = nil, = {}) 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., 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_alerts ⇒ ActiveRecord::Relation<EmployeePhoneStatus>
A relation of EmployeePhoneStatuses that are dnd alerts. Active Record Scope
75 76 77 78 79 |
# File 'app/models/employee_phone_status.rb', line 75 scope :dnd_alerts, -> { where.not(dnd_alert_threshold_minutes: nil).where.not(date_set: nil) .where(presence: 'dnd') .where("date_set + dnd_alert_threshold_minutes * INTERVAL '1 minutes' > ?", Time.current) } |
.extension_to_employee_id_hash ⇒ Object
Extension to employee id hash.
170 171 172 173 174 175 176 |
# File 'app/models/employee_phone_status.rb', line 170 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? } .to_h { |emp| [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
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'app/models/employee_phone_status.rb', line 93 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 = {}) ⇒ void
This method returns an undefined value.
This method will set the status on one or multiple employees then sync statuses to the phone system
149 150 151 152 153 154 155 |
# File 'app/models/employee_phone_status.rb', line 149 def self.initialize_and_push_presence(employees, presence, sub_presence = nil, = {}) 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, end end |
.migrate_extensions ⇒ Object
Migrate extensions.
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'app/models/employee_phone_status.rb', line 111 def self.migrate_extensions Employee.find_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
160 161 162 163 164 165 166 167 |
# File 'app/models/employee_phone_status.rb', line 160 def self.pull_presence(employees = nil, = {}) 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(queue_status_hsh = nil, observed_at: nil) ⇒ 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
273 274 275 |
# File 'app/models/employee_phone_status.rb', line 273 def self.pull_queue_status(queue_status_hsh = nil, observed_at: nil) QueueStatusPuller.call(queue_status_hsh:, observed_at:) end |
.push_presence(employees = nil, _options = {}) ⇒ Object
Pushes the presence of an employee to the phone system
131 132 133 134 135 136 137 138 139 |
# File 'app/models/employee_phone_status.rb', line 131 def self.push_presence(employees = nil, = {}) 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_options ⇒ Object
Refresh all status options.
435 436 437 |
# File 'app/models/employee_phone_status.rb', line 435 def self. find_each(&:refresh_status_options) end |
.switchboard_stream_name(employee_id, can_manage:) ⇒ String
Builds the internal Action Cable stream for one watched employee and
permission tier.
202 203 204 |
# File 'app/models/employee_phone_status.rb', line 202 def self.switchboard_stream_name(employee_id, can_manage:) EmployeePhoneStatus::BroadcastSwitchboard.stream_name(employee_id, can_manage:) end |
Instance Method Details
#auto_away_time_check ⇒ Object
Applies an automatic away transition in memory. The caller persists it
only after the PBX accepts the corresponding presence and queue updates,
leaving the transition due for the next scheduled pull when either fails.
365 366 367 368 369 370 371 372 373 374 375 376 |
# File 'app/models/employee_phone_status.rb', line 365 def auto_away_time_check if time_to_set_away? && (presence != 'away') && !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 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_status ⇒ Object
Broadcast agent status.
179 180 181 |
# File 'app/models/employee_phone_status.rb', line 179 def broadcast_agent_status self.class.broadcast_agents_status(self) end |
#broadcast_switchboard_card ⇒ void
This method returns an undefined value.
Delegates switchboard card updates after relevant commits.
209 210 211 212 213 214 |
# File 'app/models/employee_phone_status.rb', line 209 def broadcast_switchboard_card EmployeePhoneStatus::BroadcastSwitchboard.call( employee_phone_status: self, operation: :update ) end |
#contact_point ⇒ ContactPoint?
Returns the contact point this record belongs to.
53 |
# File 'app/models/employee_phone_status.rb', line 53 belongs_to :contact_point, optional: true |
#curated_presence_list ⇒ Object
Retrives a list of status options, business rules dictate we only care
about your available options, there's only one away and one dnd
467 468 469 470 471 472 473 474 475 476 477 |
# File 'app/models/employee_phone_status.rb', line 467 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 whether the record current status set after auto away.
358 359 360 |
# File 'app/models/employee_phone_status.rb', line 358 def current_status_set_after_auto_away? date_set.today? && date_set.to_time_of_day > auto_away_after end |
#dnd_alert_threshold_seconds ⇒ Object
Dnd alert threshold seconds.
342 343 344 |
# File 'app/models/employee_phone_status.rb', line 342 def dnd_alert_threshold_seconds dnd_alert_threshold_minutes * 60 if dnd_alert_threshold_minutes end |
#dnd_presence_alert? ⇒ Boolean
Returns whether the record dnd presence alert.
347 348 349 |
# File 'app/models/employee_phone_status.rb', line 347 def dnd_presence_alert? minutes_in_current_status && dnd_alert_threshold_minutes && minutes_in_current_status >= dnd_alert_threshold_minutes end |
#employee ⇒ Employee
Returns the employee this record belongs to.
51 |
# File 'app/models/employee_phone_status.rb', line 51 belongs_to :employee, inverse_of: :employee_phone_status |
#employee_phone_status_changes ⇒ ActiveRecord::Relation<EmployeePhoneStatusChange>
Returns the associated employee phone status changes.
56 |
# File 'app/models/employee_phone_status.rb', line 56 has_many :employee_phone_status_changes |
#enqueue_crm_navbar_presence_refresh ⇒ Object
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.
186 187 188 189 190 191 192 193 194 |
# File 'app/models/employee_phone_status.rb', line 186 def return if employee_id.blank? return unless previously_new_record? || saved_change_to_presence? || saved_change_to_sub_presence? || 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
401 402 403 404 |
# File 'app/models/employee_phone_status.rb', line 401 def logged_in_queue? queue_ids = (queue_statuses || {}).keys queue_ids.present? && queue_ids.all? { |queue_id| queue_logged_in?(queue_id) } end |
#minutes_in_current_status ⇒ Object
Minutes in current status.
335 336 337 338 339 |
# File 'app/models/employee_phone_status.rb', line 335 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
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 |
# File 'app/models/employee_phone_status.rb', line 307 def pull_presence(_force = false) return push_current_presence(auto_away_applied: true) if auto_away_time_check if (status_info = Phone::Pbx.instance.get_presence_status(switchvox_account_id)) 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: switchvox_account_id) return :no_status_change unless presence_changed? || sub_presence_changed? self. = status_info[:message].presence # Switchvox omits date_set for some statuses (e.g. plain "available"), so # fall back to now — the change we just observed happened by this moment. # Without this, the NOT NULL employee_phone_status_changes.date_set that # the after_save record_change writes blows up and the pulled presence is # never persisted, so the CRM never reflects the Sangoma-side change # (AppSignal #6175). self.date_set = status_info[:date_set].presence || Time.current 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 #{switchvox_account_id}" :api_call_failure end end |
#push_presence(new_presence = nil, new_sub_presence = nil, options = {}) ⇒ Boolean
Pushes the local presence settings to the pbx api
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
# File 'app/models/employee_phone_status.rb', line 284 def push_presence(new_presence = nil, new_sub_presence = nil, = {}) # First save new presence if specified if new_presence self.presence = new_presence self.sub_presence = new_sub_presence self.date_set = Time.current auto_away_applied = false else # No explicit presence: push whatever the CRM currently holds. This path # also runs right after a user change (set_presence saves the new value, # then enqueues SyncPhoneStatusWorker -> push_presence with no args), so it # MUST NOT reconcile from the PBX first — doing so reads the PBX's stale # value and clobbers the just-set status, breaking CRM -> Sangoma changes. # Inbound Sangoma -> CRM sync is owned by PullPhonePresenceWorker, which # keeps the stored presence fresh so this re-assert never pushes stale data. auto_away_applied = auto_away_time_check end push_current_presence(auto_away_applied:, options:) end |
#queue_ids_requiring_status(log_in_queue:) ⇒ Array<String>
Limits queue API writes to states that differ from the desired presence.
Unknown states are included so reconciliation remains fail-safe.
430 431 432 |
# File 'app/models/employee_phone_status.rb', line 430 def queue_ids_requiring_status(log_in_queue:) (queue_statuses || {}).keys.reject { |queue_id| queue_login_state(queue_id) == log_in_queue } end |
#queue_logged_in?(queue_id) ⇒ Boolean
Returns whether the latest observed PBX state has this queue actively
routing calls to the employee. Switchvox has emitted both the current
status-string shape and a legacy nested-hash shape over time.
412 |
# File 'app/models/employee_phone_status.rb', line 412 def queue_logged_in?(queue_id) = queue_login_state(queue_id) == true |
#queue_login_status(queue_id) ⇒ String?
Normalizes the latest observed PBX queue state.
418 419 420 421 422 423 |
# File 'app/models/employee_phone_status.rb', line 418 def queue_login_status(queue_id) raw_status = (queue_statuses || {})[queue_id.to_s] raw_status = raw_status['logged_in_status'] if raw_status.is_a?(Hash) raw_status = raw_status.logged_in_status if raw_status.respond_to?(:logged_in_status) raw_status.to_s.presence end |
#refresh_status_options ⇒ Object
Update presence status list
440 441 442 443 444 445 446 |
# File 'app/models/employee_phone_status.rb', line 440 def return unless switchvox_account_id && switchvox_account_id > 0 option_list = Phone::Pbx.instance. switchvox_account_id update_attribute :status_options, option_list option_list end |
#refresh_switchboard ⇒ void
This method returns an undefined value.
Delegates refresh when this employee enters the switchboard roster.
239 240 241 242 243 244 |
# File 'app/models/employee_phone_status.rb', line 239 def refresh_switchboard EmployeePhoneStatus::BroadcastSwitchboard.call( employee_phone_status: self, operation: :refresh ) end |
#remove_switchboard_card ⇒ void
This method returns an undefined value.
Delegates removal when this employee leaves the switchboard roster.
219 220 221 222 223 224 |
# File 'app/models/employee_phone_status.rb', line 219 def remove_switchboard_card EmployeePhoneStatus::BroadcastSwitchboard.call( employee_phone_status: self, operation: :remove ) end |
#replace_switchboard_card ⇒ void
This method returns an undefined value.
Delegates an unconditional card replacement for related visible changes.
229 230 231 232 233 234 |
# File 'app/models/employee_phone_status.rb', line 229 def replace_switchboard_card EmployeePhoneStatus::BroadcastSwitchboard.call( employee_phone_status: self, operation: :replace ) end |
#should_be_in_queue?(presence: self.presence, sub_presence: self.sub_presence) ⇒ Boolean
Checks whether a presence accepts phone-queue calls.
Optional values let callers evaluate a proposed status without mutating
the record first.
391 392 393 394 395 396 |
# File 'app/models/employee_phone_status.rb', line 391 def should_be_in_queue?(presence: self.presence, sub_presence: self.sub_presence) normalized_presence = presence.to_s.downcase.squish normalized_sub_presence = sub_presence.to_s.downcase.tr('_-', ' ').squish normalized_presence == 'available' && normalized_sub_presence != 'non queue' end |
#status_id_for_presence ⇒ Object
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
451 452 453 454 |
# File 'app/models/employee_phone_status.rb', line 451 def status_id_for_presence if .blank? curated_presence_list.find { |so| so[:presence].presence == presence.to_s.presence && so[:sub_presence].presence == sub_presence.to_s.presence }.try(:[], :id) end |
#sub_presence_options ⇒ Object
Sub presence options.
379 380 381 |
# File 'app/models/employee_phone_status.rb', line 379 def curated_presence_list.pluck(:sub_presence).compact end |
#time_to_set_away? ⇒ Boolean
Checks if it's time to go on auto away
352 353 354 355 |
# File 'app/models/employee_phone_status.rb', line 352 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_list ⇒ Object
Filters out status options
457 458 459 460 461 462 463 |
# File 'app/models/employee_phone_status.rb', line 457 def valid_presence_list .select do |so| so['presence'] == 'available' || (so['presence'] == 'away' && so['sub_presence'].blank?) || so['presence'] == 'dnd' end end |