Class: CrmNavbarCounts

Inherits:
Object
  • Object
show all
Defined in:
app/services/crm_navbar_counts.rb

Overview

Computes the badge counts shown in the CRM navbar for a given user. Single
source of truth shared by:

  • the navbar partial (initial render)
  • the badge fragment partials (replace targets for Turbo Stream broadcasts)
  • the resync endpoint

Each method returns just the integer the corresponding badge displays, so the
partials stay dumb. Manager-only "all unread" counts and watch-list-derived
scopes live here too, again to keep the partials free of conditional logic.

Caching: the four totals fed to badge partials (sms_total, voicemail_total,
unread_email, pinned_total) are wrapped in a short-TTL Rails.cache fetch.
Initial CRM page renders re-hit these counts on every full response and the
underlying queries (joins on watch lists, full_name lookups, search-result
joins) were a top contributor to AppSignal performance incident #339 and
kin — the navbar partial alone showed up at 65–693ms per request. With a
30s window the badges stay near-fresh while page load drops a measurable
slice. Authoritative liveness is preserved because CrmNavbarRefreshWorker
busts these cache keys before broadcasting a replace, so the broadcast
payload always reflects the latest data.

Constant Summary collapse

CACHE_TTL =
30.seconds
CACHED_METHODS =
%i[sms_total voicemail_total unread_email pinned_total].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(user) ⇒ void

Create a CrmNavbarCounts instance for the given user.

Parameters:

  • user (Employee)

    The user whose CRM navbar badge totals will be computed and cached.



31
32
33
# File 'app/services/crm_navbar_counts.rb', line 31

def initialize(user)
  @user = user
end

Instance Attribute Details

#userObject (readonly)

Returns the value of attribute user.



35
36
37
# File 'app/services/crm_navbar_counts.rb', line 35

def user
  @user
end

Class Method Details

.bust_cache(user) ⇒ void

This method returns an undefined value.

Drops the cached totals for a user so the next read recomputes. Called by
CrmNavbarRefreshWorker before broadcasting a badge replace, so the live
update reflects the freshest data instead of whatever was cached when the
event fired.

Parameters:

  • user (Employee, nil)

    The user whose cached navbar totals should be cleared. No-op when nil.



44
45
46
47
48
# File 'app/services/crm_navbar_counts.rb', line 44

def self.bust_cache(user)
  return unless user

  CACHED_METHODS.each { |m| Rails.cache.delete(cache_key(user, m)) }
end

.cache_key(user, method_name) ⇒ Array(String, Integer, Symbol)

Builds the Rails.cache key for one cached badge total.

Parameters:

  • user (Employee)

    The user owning the cached value.

  • method_name (String, Symbol)

    The cached method identifier.

Returns:

  • (Array(String, Integer, Symbol))

    Namespaced cache key tuple.



55
56
57
# File 'app/services/crm_navbar_counts.rb', line 55

def self.cache_key(user, method_name)
  ["crm_navbar_counts", user.id, method_name]
end

Instance Method Details

#all_unread_emailInteger?

Compute the total unread inbound emails across the account for manager users.

Returns:

  • (Integer, nil)

    The count of open inbound email activities that have a non-nil communication_id when the current user is a manager; nil otherwise.



147
148
149
150
151
# File 'app/services/crm_navbar_counts.rb', line 147

def all_unread_email
  return unless manager?

  Activity.open_activities.inbound_emails.where.not(communication_id: nil).size
end

#all_unread_smsObject



83
84
85
# File 'app/services/crm_navbar_counts.rb', line 83

def all_unread_sms
  SmsMessage.inbound.unread.size if manager?
end

#all_unread_voicemailsObject



114
115
116
# File 'app/services/crm_navbar_counts.rb', line 114

def all_unread_voicemails
  CallRecord.voicemails.unread.size if manager?
end

#communications_totalObject

Communications (combined)

Aggregate "unread for me" badge for the unified Communications offcanvas
in the navbar (sms + voicemails + email). Mirrors the per-channel _total
methods so the visual badge matches what the offcanvas surfaces in its
three sections. Each underlying call is already memoised through its own
query — this just sums the integers.



160
161
162
# File 'app/services/crm_navbar_counts.rb', line 160

def communications_total
  sms_total + voicemail_total + unread_email
end

#global_sms_numbersObject



91
92
93
# File 'app/services/crm_navbar_counts.rb', line 91

def global_sms_numbers
  SmsMessage::SMS_GLOBAL_NUMBERS
end

#manager?Boolean

Determine whether the current user is an account manager.
Memoizes the result for subsequent calls.

Returns:

  • (Boolean)

    true if the user's account reports is_manager?, false otherwise.



191
192
193
194
195
# File 'app/services/crm_navbar_counts.rb', line 191

def manager?
  return @manager unless @manager.nil?

  @manager = user.try(:account).try(:is_manager?).to_b
end

#monitor_group_sms_numbers?Boolean

Returns:

  • (Boolean)


95
96
97
# File 'app/services/crm_navbar_counts.rb', line 95

def monitor_group_sms_numbers?
  user.monitor_group_sms_numbers
end

#monitor_unlinked_extensions?Boolean

Returns:

  • (Boolean)


118
119
120
# File 'app/services/crm_navbar_counts.rb', line 118

def monitor_unlinked_extensions?
  user.monitor_unlinked_extensions
end

#my_sms_numbersObject



87
88
89
# File 'app/services/crm_navbar_counts.rb', line 87

def my_sms_numbers
  @my_sms_numbers ||= Employee.all_active_employees_sms_numbers(rep_ids: watched_employee_ids)
end

#pinned_totalInteger

Pinned items

Counts the rows of the user's currently-pinned Search (if any). Each user
has at most one pinned Search at a time (Search#remove_other_pins enforces

Counts the current user's pinned search results.
The value is cached per user.

Returns:

  • (Integer)

    The number of SearchResult records whose associated Search has employee_id equal to the user and pinned set to true.



172
173
174
175
176
177
178
# File 'app/services/crm_navbar_counts.rb', line 172

def pinned_total
  cached(:pinned_total) do
    SearchResult.joins(:search)
                .where(searches: { employee_id: user.id, pinned: true })
                .size
  end
end

#sms_totalInteger

Total unread SMS messages to display in the user's CRM navbar.
Includes unread inbound SMS for the user's watched numbers and also includes unread global SMS when the user has group SMS monitoring enabled.

Returns:

  • (Integer)

    The total unread SMS count (0 or greater).



64
65
66
67
68
69
70
# File 'app/services/crm_navbar_counts.rb', line 64

def sms_total
  cached(:sms_total) do
    count = unread_my_sms
    count += unread_global_sms if user.monitor_group_sms_numbers
    count
  end
end

#unread_emailInteger

Count unread inbound emails assigned to the user's watched employees, excluding records with no communication_id.

Returns:

  • (Integer)

    The number of unread inbound Activity records whose assigned_resource_id is in the user's watched employee ids and that have a non-nil communication_id.



135
136
137
138
139
140
141
142
# File 'app/services/crm_navbar_counts.rb', line 135

def unread_email
  cached(:unread_email) do
    Activity.open_activities.inbound_emails
            .where(assigned_resource_id: watched_employee_ids)
            .where.not(communication_id: nil)
            .size
  end
end

#unread_global_smsObject



79
80
81
# File 'app/services/crm_navbar_counts.rb', line 79

def unread_global_sms
  SmsMessage.for_numbers(SmsMessage::SMS_GLOBAL_NUMBERS).inbound.unread.size
end

#unread_my_smsInteger

Count unread inbound SMS messages for the user's monitored SMS numbers.

Returns:

  • (Integer)

    The number of inbound SmsMessage records that are unread and addressed to the user's monitored SMS numbers.



75
76
77
# File 'app/services/crm_navbar_counts.rb', line 75

def unread_my_sms
  SmsMessage.for_numbers(my_sms_numbers).inbound.unread.size
end

#voicemail_destination_filterObject

Mirrors what the voicemails-by-watch-list link expects: stringified watch
list ids, optionally appended with the "null" sentinel that
CallRecord#destination_filter recognises as "include unlinked extensions".



125
126
127
128
129
# File 'app/services/crm_navbar_counts.rb', line 125

def voicemail_destination_filter
  filter = watched_employee_ids.map(&:to_s)
  filter << 'null' if monitor_unlinked_extensions?
  filter
end

#voicemail_totalInteger

Computes the integer total of unread voicemails shown in the CRM navbar for the user.
The result is cached per user for 30 seconds.

Returns:

  • (Integer)

    The count of unread voicemail CallRecord entries for the user's watched employee ids; includes unlinked (unassigned) extensions when monitor_unlinked_extensions? is true.



104
105
106
107
108
109
110
111
112
# File 'app/services/crm_navbar_counts.rb', line 104

def voicemail_total
  cached(:voicemail_total) do
    if monitor_unlinked_extensions?
      CallRecord.voicemails.unread.for_destination_employees_or_unlinked(watched_employee_ids).size
    else
      CallRecord.voicemails.unread.for_destination_employees(watched_employee_ids).size
    end
  end
end

#watched_employee_idsArray<Integer>

List of employee IDs the user is watching.

Returns:

  • (Array<Integer>)

    Employee IDs from the user's effective watch list.



183
184
185
# File 'app/services/crm_navbar_counts.rb', line 183

def watched_employee_ids
  @watched_employee_ids ||= user.effective_watch_list_ids
end