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.



144
145
146
147
148
# File 'app/services/crm_navbar_counts.rb', line 144

def all_unread_email
  return unless manager?

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

#all_unread_smsObject



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

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

#all_unread_voicemailsObject



112
113
114
# File 'app/services/crm_navbar_counts.rb', line 112

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.



157
158
159
# File 'app/services/crm_navbar_counts.rb', line 157

def communications_total
  sms_total + voicemail_total + unread_email
end

#global_sms_numbersObject



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

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.



188
189
190
191
192
# File 'app/services/crm_navbar_counts.rb', line 188

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

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

#monitor_group_sms_numbers?Boolean

Returns:

  • (Boolean)


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

def monitor_group_sms_numbers?
  user.monitor_group_sms_numbers
end

#monitor_unlinked_extensions?Boolean

Returns:

  • (Boolean)


116
117
118
# File 'app/services/crm_navbar_counts.rb', line 116

def monitor_unlinked_extensions?
  user.monitor_unlinked_extensions
end

#my_sms_numbersObject



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

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.



169
170
171
172
173
174
175
# File 'app/services/crm_navbar_counts.rb', line 169

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



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

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.



132
133
134
135
136
137
138
139
# File 'app/services/crm_navbar_counts.rb', line 132

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



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

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.



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

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



123
124
125
126
127
# File 'app/services/crm_navbar_counts.rb', line 123

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.



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

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.



180
181
182
# File 'app/services/crm_navbar_counts.rb', line 180

def watched_employee_ids
  @watched_employee_ids ||= user.effective_watch_list_ids
end