Class: PhoneQueue
- Inherits:
-
Object
- Object
- PhoneQueue
- Defined in:
- app/models/phone_queue.rb
Overview
One phone queue and the reps who answer it.
This is the only place that knows how queue membership is stored. Today that
is the Switchvox-sourced +employee_phone_statuses.queue_statuses+ JSONB, keyed
by the PBX's queue account id. Switchvox is being retired for Twilio +
TaskRouter, where membership becomes Worker attributes — when that lands,
+#members+ changes and nothing else does.
Constant Summary collapse
- REGISTRY =
queue account id => [extension, display name].
Pinned rather than derived from a DISTINCT over queue_call_logs: a queue
with no recent calls has no rows there, so deriving it would silently drop
exactly the quiet queues someone opens this page to investigate. { 1141 => [615, 'Operator'], 1142 => [612, 'Sales'], 1143 => [613, 'Tech 1'], 1147 => [600, 'Tech 24x7'], 1164 => [618, 'Spanish'], 1204 => [621, 'Management'], 1225 => [623, 'Accounting'], 1276 => [630, 'Sales_Homeowner'], 1277 => [631, 'Sales_Commercial'], 1278 => [632, 'Sales_BG'], 1279 => [633, 'Sales_Trade'], 1280 => [634, 'Sales_Ecommerce'] }.each_value(&:freeze).freeze
- ACTIVITY_WINDOW_DAYS =
Window used to judge whether a queue is still in service.
90
Instance Attribute Summary collapse
-
#account_id ⇒ Object
readonly
Returns the value of attribute account_id.
-
#extension ⇒ Object
readonly
Returns the value of attribute extension.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
Class Method Summary collapse
-
.activity ⇒ Hash{Integer => Hash}
Traffic per queue in one grouped query, so a page can show every queue's activity without a query each.
-
.all ⇒ Array<PhoneQueue>
Built fresh on every call.
- .find(account_id) ⇒ PhoneQueue?
- .for_extension(extension) ⇒ PhoneQueue?
-
.member_counts ⇒ Hash{Integer => Integer}
Roster size per queue in one grouped query.
Instance Method Summary collapse
-
#==(other) ⇒ Boolean
(also: #eql?)
True when +other+ is the same queue.
-
#hash ⇒ Integer
Namespaced so a queue never collides with a bare Integer in a Hash or Set.
-
#initialize(account_id, extension, name) ⇒ PhoneQueue
constructor
A new instance of PhoneQueue.
-
#members ⇒ Array<Employee>
Reps the PBX will ring for this queue, whether or not they are currently logged in — a logged-off member is still roster, and the coverage table says so rather than pretending they don't exist.
-
#other_queue_counts ⇒ Hash{Integer => Integer}
How many other queues each member also answers, for the dedicated/shared split.
-
#to_param ⇒ String
The account id, so routes read /queue_coverage?queue_id=1143.
-
#unlisted_answerers(since: 90) ⇒ Array<Employee>
Employee ids seen answering this queue in +since+ that the roster doesn't claim.
Constructor Details
#initialize(account_id, extension, name) ⇒ PhoneQueue
Returns a new instance of PhoneQueue.
97 98 99 100 101 |
# File 'app/models/phone_queue.rb', line 97 def initialize(account_id, extension, name) @account_id = account_id @extension = extension @name = name end |
Instance Attribute Details
#account_id ⇒ Object (readonly)
Returns the value of attribute account_id.
34 35 36 |
# File 'app/models/phone_queue.rb', line 34 def account_id @account_id end |
#extension ⇒ Object (readonly)
Returns the value of attribute extension.
34 35 36 |
# File 'app/models/phone_queue.rb', line 34 def extension @extension end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
34 35 36 |
# File 'app/models/phone_queue.rb', line 34 def name @name end |
Class Method Details
.activity ⇒ Hash{Integer => Hash}
Traffic per queue in one grouped query, so a page can show every queue's
activity without a query each.
Answers "is this queue still real?" — the registry is hand-maintained, and
queue_call_logs holds another dozen account ids (Tech 2, Customer_Contact,
Sales Overflow…) whose last call was between 2017 and 2025. Note the ids are
reused: 1204 was "Operator Overflow" in 2019 and is "Management" now, so the
registry pins today's meaning and history is read by id, never by name.
46 47 48 49 50 51 52 53 54 55 |
# File 'app/models/phone_queue.rb', line 46 def self.activity QueueCallLog.where(queue_account_id: REGISTRY.keys) .group(:queue_account_id) .pluck( :queue_account_id, Arel.sql("COUNT(*) FILTER (WHERE start_date_cst > CURRENT_DATE - #{ACTIVITY_WINDOW_DAYS})"), Arel.sql('MAX(start_date_cst)') ) .to_h { |acct, calls, last| [acct, { calls: calls.to_i, last_call_on: last }] } end |
.all ⇒ Array<PhoneQueue>
Built fresh on every call. Memoising at class level would pin each
instance — and its cached #members — for the life of the process, so a rep
joining a queue would never show up until a deploy.
78 79 80 |
# File 'app/models/phone_queue.rb', line 78 def self.all REGISTRY.map { |account_id, (extension, name)| new(account_id, extension, name) }.sort_by(&:name) end |
.find(account_id) ⇒ PhoneQueue?
84 85 86 |
# File 'app/models/phone_queue.rb', line 84 def self.find(account_id) all.find { |queue| queue.account_id == account_id.to_i } end |
.for_extension(extension) ⇒ PhoneQueue?
90 91 92 |
# File 'app/models/phone_queue.rb', line 90 def self.for_extension(extension) all.find { |queue| queue.extension == extension.to_i } end |
.member_counts ⇒ Hash{Integer => Integer}
Roster size per queue in one grouped query.
60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'app/models/phone_queue.rb', line 60 def self.member_counts counts = Hash.new(0) # Scoped to active employees to match #members. A departed rep whose phone # status was never cleaned up would otherwise be counted as roster, hiding # the "taking calls, no roster" warning while coverage stayed empty. EmployeePhoneStatus.where.not(queue_statuses: nil) .where(employee_id: Employee.active_employees.select(:id)) .pluck(:queue_statuses).each do |statuses| (statuses || {}).each_key { |key| counts[key.to_i] += 1 } end counts end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
Returns true when +other+ is the same queue.
146 |
# File 'app/models/phone_queue.rb', line 146 def ==(other) = other.is_a?(PhoneQueue) && other.account_id == account_id |
#hash ⇒ Integer
Namespaced so a queue never collides with a bare Integer in a Hash or Set.
152 |
# File 'app/models/phone_queue.rb', line 152 def hash = [self.class, account_id].hash |
#members ⇒ Array<Employee>
Reps the PBX will ring for this queue, whether or not they are currently
logged in — a logged-off member is still roster, and the coverage table
says so rather than pretending they don't exist.
108 109 110 111 112 113 114 |
# File 'app/models/phone_queue.rb', line 108 def members @members ||= Employee.active_employees .where(id: statuses_for_queue.map(&:employee_id)) .includes(:employee_work_schedules, :employee_phone_status, :employee_record) .order(:full_name) .to_a end |
#other_queue_counts ⇒ Hash{Integer => Integer}
How many other queues each member also answers, for the dedicated/shared
split. A rep in six queues is not six reps of capacity.
135 136 137 138 139 |
# File 'app/models/phone_queue.rb', line 135 def other_queue_counts @other_queue_counts ||= EmployeePhoneStatus .where(employee_id: members.map(&:id)) .to_h { |eps| [eps.employee_id, (queue_keys(eps) - [account_id.to_s]).size] } end |
#to_param ⇒ String
Returns the account id, so routes read /queue_coverage?queue_id=1143.
142 |
# File 'app/models/phone_queue.rb', line 142 def to_param = account_id.to_s |
#unlisted_answerers(since: 90) ⇒ Array<Employee>
Employee ids seen answering this queue in +since+ that the roster doesn't
claim. Usually overflow or a redirect rather than real membership, so they
are surfaced as a note and never folded into the headcount — merging them
would inflate coverage.
123 124 125 126 127 128 129 |
# File 'app/models/phone_queue.rb', line 123 def unlisted_answerers(since: 90) ids = QueueCallLog.where(queue_account_id: account_id) .where(start_date_cst: since.days.ago.to_date..) .where.not(member_party_id: nil) .distinct.pluck(:member_party_id) Employee.where(id: ids - members.map(&:id)).order(:full_name).to_a end |