Class: DailyFocusController

Inherits:
CrmController show all
Defined in:
app/controllers/daily_focus_controller.rb

Constant Summary collapse

DAILY_FOCUS_TOOL_SERVICES =
DailyFocus::TOOL_SERVICE_KEYS
DAILY_FOCUS_TOOL_HANDLES =
%w[sales-management app-db support-cases content-search].freeze
WORK_DAY_INDICATOR_UNSET =
Object.new.freeze
DAILY_FOCUS_BRIEFING_ACTIVE_STATUSES =

Row statuses that mean this rep has (or shares) a Daily Focus artifact for the day.

%i[processing pending approved failed covered].freeze

Constants included from Controllers::ReferenceFindable

Controllers::ReferenceFindable::ID_EMBEDDED_PATTERNS

Constants included from Controllers::AnalyticsEvents

Controllers::AnalyticsEvents::MAX_QUEUED_EVENTS, Controllers::AnalyticsEvents::SESSION_KEY

Constants included from Controllers::ErrorRendering

Controllers::ErrorRendering::NON_CONTENT_PATH_PREFIXES

Constants included from Www::SeoHelper

Www::SeoHelper::AWARDS, Www::SeoHelper::CA_ADDRESS, Www::SeoHelper::CA_BUSINESS_HOURS, Www::SeoHelper::CA_CONTACT_POINT, Www::SeoHelper::CA_CURRENCIES, Www::SeoHelper::CA_DESCRIPTION, Www::SeoHelper::CA_FOUNDING_DATE, Www::SeoHelper::CA_GLOBAL_LOCATION_NUMBER, Www::SeoHelper::CA_LEGAL_NAME, Www::SeoHelper::CA_LOCAL_BUSINESS, Www::SeoHelper::CA_ONLINE_STORE, Www::SeoHelper::CA_RETURN_POLICY, Www::SeoHelper::CA_SALES_DEPARTMENT, Www::SeoHelper::CA_SERVICE_AREA, Www::SeoHelper::CA_URL, Www::SeoHelper::CA_VAT_ID, Www::SeoHelper::CA_WAREHOUSE_DEPARTMENT, Www::SeoHelper::CA_WAREHOUSE_HOURS, Www::SeoHelper::COMPANY_EMAIL, Www::SeoHelper::COMPANY_LOGO, Www::SeoHelper::COMPANY_NAME, Www::SeoHelper::COMPANY_SLOGAN, Www::SeoHelper::EXPERTISE, Www::SeoHelper::FAX_NUMBER, Www::SeoHelper::GS1_COMPANY_PREFIX, Www::SeoHelper::ISO6523_CODE, Www::SeoHelper::PAYMENT_METHODS, Www::SeoHelper::PHONE_NUMBER, Www::SeoHelper::PRIMARY_NAICS, Www::SeoHelper::REFUND_TYPE, Www::SeoHelper::RETURN_FEES, Www::SeoHelper::RETURN_METHOD, Www::SeoHelper::RETURN_POLICY_CATEGORY, Www::SeoHelper::SECONDARY_NAICS, Www::SeoHelper::SOCIAL_PROFILES, Www::SeoHelper::US_ADDRESS, Www::SeoHelper::US_BUSINESS_HOURS, Www::SeoHelper::US_CONTACT_POINT, Www::SeoHelper::US_CURRENCIES, Www::SeoHelper::US_DESCRIPTION, Www::SeoHelper::US_FOUNDING_DATE, Www::SeoHelper::US_GLOBAL_LOCATION_NUMBER, Www::SeoHelper::US_IMAGE, Www::SeoHelper::US_LEGAL_NAME, Www::SeoHelper::US_LOCAL_BUSINESS, Www::SeoHelper::US_ONLINE_STORE, Www::SeoHelper::US_RETURN_POLICY, Www::SeoHelper::US_SALES_DEPARTMENT, Www::SeoHelper::US_SERVICE_AREA, Www::SeoHelper::US_TAX_ID, Www::SeoHelper::US_URL, Www::SeoHelper::US_WAREHOUSE_DEPARTMENT, Www::SeoHelper::US_WAREHOUSE_HOURS

Constants included from IconHelper

IconHelper::CUSTOM_ICON_MAP, IconHelper::CUSTOM_SVG_DIR, IconHelper::DEFAULT_FAMILY

Instance Method Summary collapse

Methods inherited from CrmController

#access_denied, #context_id, #context_object, #crm_home_path, #current_ability, #default_url_options, #download_temp, #get_tempfile_path_for_download, #initialize_crm_lazy_chunks, #record_not_found, #redirect_to_job_or_fallback, #render_edit_action, #set_context, #set_download_path, #stash_file_for_temp_download

Methods inherited from ApplicationController

#account_impersonated?, #add_to_flash, #append_token, #bypass_forgery_protection?, #chat_enabled?, #cloudflare_cleared?, #default_catalog, #default_url_options, #enable_turbo_frames, #find_publication, #fix_invalid_accept_header, #init_js_utils, #is_globals_call?, #layout_by_resource, #locale_store, #redirect_to, #require_employee_for_crm, #set_base_host, #set_real_ip, #set_report_errors_for, #should_render_layout?, #stamp_impersonation_context, #warmlyyours_canada_ip?, #warmlyyours_ip?, #y

Methods included from Controllers::ReturnPathHandling

#check_for_return_path, #redirect_to_return_path_or_default

Methods included from Controllers::AnalyticsEvents

#consume_queued_analytics_events, #track_event

Methods included from Controllers::DeviceDetection

#device_detector, #is_ie?

Methods included from Controllers::SubdomainDetection

#is_crm_request?, #is_www_request?, #json_request?

Methods included from Controllers::TrackingDetection

#bot_request?, #gdpr_country?, #gdpr_country_data, #prevent_bots, #set_tracking_cookie, #track_visitor?

Methods included from Controllers::AcceleratedFileSending

#send_file_accelerated, #send_upload_accelerated

Methods included from Controllers::ErrorRendering

#excp_string, #mail_to_for_error_reporting, #render_400, #render_404, #render_406, #render_410, #render_500, #render_invalid_authenticity_token, #render_ip_spoof_error, #safe_referer_or_fallback

Methods included from Controllers::TurnstileVerification

#load_turnstile_script_tag, #turnstile_lazy_widget, #turnstile_script_tag, #turnstile_widget, #validate_turnstile!

Methods included from Controllers::CloudflareCaching

edge_cached, #edge_cached_action?, #reset_cloudflare_cache, #set_cloudflare_cache, #skip_session

Methods included from Controllers::Webpackable

#preload_webpack_fonts, #webpack_css_include, #webpack_css_url, #webpack_js_include, #wpd_is_running?

Methods included from Controllers::Localizable

#cloudflare_country_locale, #determine_request_locale, #geocoder_locale, #guest_user_locale_check, #locale_optional_www_auth_path?, #param_locale, #set_locale, #set_request_locale, #skip_localization?, #warmlyyours_ip_locale

Methods included from Controllers::Authenticable

#access_denied, #authenticate_account, #authenticate_account!, #authenticate_account_from_login_token!, #authenticate_account_from_token!, #check_is_a_manager, #check_is_a_sales_manager, #check_is_an_admin, #check_is_an_employee, #check_party, #clear_mismatched_guest_user, #create_guest_user, #credentials?, #current_or_guest_user, #current_or_guest_user_id_read_only, #current_user, #devise_mapping, #fully_logged_in?, #generate_bot_id, #guest_user, #identifiable?, #init_current_user, #initialize_guest, #load_context_user, #logging_in, #resource, #resource_name, #restrict_access_for_non_employees, #scrubbed_request_path, #user_object, #warn_on_session_guest_id_leak

Methods included from ApplicationHelper

#better_number_to_currency, #check_force_logout, #check_or_cross, #check_or_times, #error_messages, #general_disclaimer_on_product_installation_and_local_codes, #gridjs_from_html_table, #gridjs_table, #is_wy_ip, #line_break, #parent_layout, #pass_or_fail, #render_error_messages_list, #render_video_card, #resolved_auth_form_turbo_frame, #return_path_or, #safe_css_color, #set_return_path_if_present, #set_section_if_present, #tab_frame_id, #to_underscore, #track_page?, #turbo_section_wrapper, #turbo_tabs_request?, #url_on_same_domain_as_request, #widget_index_daily_focus_index_path, #working_hours?, #yes_or_no, #yes_or_no_highlighted, #yes_or_no_with_check_or_cross, #youtube_video

Methods included from UppyUploaderHelper

#file_uploader, #image_uploader, #large_file_uploader_s3, #lead_sketch_uploader, #rma_image_uploader, #rma_image_uploader_s3, #uppy_uploader, #video_uploader

Methods included from Www::ImagesHelper

#image_asset_tag, #image_asset_url

Methods included from Www::SeoHelper

#add_page_schema, #canada?, #company_social_links, #ensure_context_json, #json_ld_script_tag, #local_business_schema, #online_store_id, #online_store_schema, #page_main_entity, #page_main_entity_json, #render_auto_collection_page_schema, #render_collection_page_schema, #render_local_business_schema, #render_online_store_schema, #render_page_schemas, #render_page_video_schemas, #render_webpage_schema, #render_webpage_schema_with_collections, #usa?

Methods included from UrlsHelper

#catalog_breadcrumb_links, #catalog_link, #catalog_link_for_product_line, #catalog_link_for_sku, #cms_link, #delocalized_path, #path_to_sales_product_sku, #path_to_sales_product_sku_for_product_line, #path_to_sales_product_sku_for_product_line_slug, #product_line_from_catalog_link, #protocol_neutral_url, #sanitize_external_url, #valid_external_url?

Methods included from IconHelper

#account_nav_icon, #fa_icon, #star_rating_html

Instance Method Details

#approveObject

Approve a daily focus briefing and mark it as approved for its target employee.
Updates the conversation's user and title (using the conversation's daily_focus_date), removes the daily_focus_target_employee_id and daily_focus_status metadata keys, enqueues an approval email to the employee, and responds by replacing the review row and stats via Turbo Stream or by redirecting to the review page.
If the conversation has no daily_focus_target_employee_id, responds with HTTP 422 Unprocessable Entity.

Parameters:

  • id (Integer, String)
    • The AssistantConversation id provided in params[:id].


52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'app/controllers/daily_focus_controller.rb', line 52

def approve
  conversation = AssistantConversation.find(params[:id])
  target_id = conversation.daily_focus_target_employee_id
  return head(:unprocessable_entity) unless target_id

  rep = Employee.find(target_id)
  brief_date = review_date_param_for(conversation)
  conversation.update!(
    user_id: rep.id,
    title: "Daily Focus — #{brief_date.strftime('%A, %B %-d, %Y')}"
  )
  AssistantConversation.where(id: conversation.id).update_all("metadata = metadata - 'daily_focus_target_employee_id' - 'daily_focus_status'")
  conversation.reload

  InternalMailer.daily_focus_approved(employee: rep, conversation: conversation).deliver_later

  respond_to do |format|
    format.turbo_stream do
      rd = review_date_param_for(conversation)
      covered_ids = Array(conversation.daily_focus_covered_employee_ids).map(&:to_i)
      covered_by_id = Employee.where(id: covered_ids).index_by(&:id)
      covered_streams = covered_ids.filter_map do |cid|
        cov = covered_by_id[cid]
        next unless cov

        turbo_stream.replace(
          "daily-focus-row-#{cov.id}",
          partial: 'daily_focus/review_row',
          locals: review_row_locals(cov, :covered, conversation, for_date: rd, covering_rep: rep)
        )
      end
      render turbo_stream: [
        turbo_stream.replace(
          "daily-focus-row-#{rep.id}",
          partial: 'daily_focus/review_row',
          locals: review_row_locals(rep, :approved, conversation, for_date: rd)
        ),
        *covered_streams,
        turbo_stream.replace("daily-focus-stats", partial: 'daily_focus/stats_cards', locals: stats_card_locals(for_date: rd))
      ]
    end
    format.html { redirect_to review_daily_focus_path(date: review_date_param_for(conversation)), notice: "Daily focus approved for #{rep.full_name}" }
  end
end

#generateObject

Creates a new "Daily Focus" briefing for the specified employee, marks it as processing, shares it with sales leadership, enqueues background analysis, and updates the review UI.

The action:

  • creates an Assistant conversation titled for the employee and today's date and stores daily-focus metadata on it,
  • shares the conversation with sales leadership,
  • enqueues DailyFocusAnalysisRepWorker to generate the briefing content,
  • responds with a Turbo Stream that replaces the employee's review row and the stats cards, or redirects to the review page with a notice for HTML requests.


160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'app/controllers/daily_focus_controller.rb', line 160

def generate
  employee = Employee.find(params[:employee_id])

  cleanup_stale_daily_focus(employee)

  title = "Daily Focus — #{employee.full_name}#{Date.current.strftime('%A, %B %-d, %Y')}"
  conversation = Assistant::SunnyAgent.create!(user: current_user, title: title)
  conversation.update!(metadata: conversation..merge(
    'daily_focus_date' => Date.current.iso8601,
    'daily_focus_target_employee_id' => employee.id,
    'daily_focus_covered_employee_ids' => [],
    'daily_focus_status' => 'processing',
    'conversation_type' => 'daily_briefing',
    'tool_services' => DailyFocus::TOOL_SERVICE_KEYS
  ))
  DailyFocus::ConversationSharing.share_with_sales_leadership!(conversation, shared_by: current_user)

  DailyFocusAnalysisRepWorker.perform_async(employee.id, conversation.id, [])

  respond_to do |format|
    format.turbo_stream do
      render turbo_stream: [
        turbo_stream.replace(
          "daily-focus-row-#{employee.id}",
          partial: 'daily_focus/review_row',
          locals: review_row_locals(employee, :processing, conversation, for_date: Date.current)
        ),
        turbo_stream.replace("daily-focus-stats", partial: 'daily_focus/stats_cards', locals: stats_card_locals(for_date: Date.current))
      ]
    end
    format.html { redirect_to review_daily_focus_path(date: Date.current.iso8601), notice: "Daily focus generation queued for #{employee.full_name}" }
  end
end

#generate_with_instructionsObject

Creates a new "Daily Focus" briefing conversation for the specified employee, seeds it with tool-service defaults and a prefill instruction prompt, shares it with sales leadership, and redirects the user to the assistant view for that conversation.

The action:

  • Loads the target employee and removes any stale/failed processing briefings for that employee for today.
  • Creates an Assistant::SunnyAgent conversation and sets metadata including daily_focus_date, daily_focus_target_employee_id, daily_focus_status ('processing'), conversation_type ('daily_briefing'), and tool_services (the configured daily-focus services).
  • Shares the conversation with sales leadership.
  • Stores a long instructional prefill in conversation.draft_prefill and default tool handles in conversation.default_tool_handles so they survive any redirect and a refresh.
  • Redirects to the assistant view for the newly created conversation.


203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'app/controllers/daily_focus_controller.rb', line 203

def generate_with_instructions
  employee = Employee.find(params[:employee_id])

  cleanup_stale_daily_focus(employee)

  today = Date.current.strftime('%A, %B %-d, %Y')

  title = "Daily Focus — #{employee.full_name}#{today}"
  conversation = Assistant::SunnyAgent.create!(user: current_user, title: title)
  conversation.update!(metadata: conversation..merge(
    'daily_focus_date' => Date.current.iso8601,
    'daily_focus_target_employee_id' => employee.id,
    'daily_focus_covered_employee_ids' => [],
    'daily_focus_status' => 'processing',
    'conversation_type' => 'daily_briefing',
    'tool_services' => DailyFocus::TOOL_SERVICE_KEYS,
    'draft_prefill' => DailyFocus::Prompt.instructions_prefill(primary_employee: employee, covered_employees: []),
    'default_tool_handles' => DAILY_FOCUS_TOOL_HANDLES
  ))
  DailyFocus::ConversationSharing.share_with_sales_leadership!(conversation, shared_by: current_user)

  redirect_to assistant_path(conversation)
end

#rejectObject

Rejects a daily focus briefing, removes its processing lock, destroys the associated conversation, and updates the review UI.

Finds the AssistantConversation by params[:id], releases its processing lock if held, and destroys it. If the conversation lacks a linked target employee, responds with HTTP 422 Unprocessable Entity.

Responds to:

  • turbo_stream: replaces the target employee's review row (marking it as :missing) and refreshes the stats cards for the briefing date.
  • html: redirects to the review page for the briefing date with a notice.

The action uses the conversation's stored daily_focus_date to determine the review date and whether generation of missing briefings should be allowed in the replaced row.



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'app/controllers/daily_focus_controller.rb', line 107

def reject
  conversation = AssistantConversation.find(params[:id])
  target_id = conversation.daily_focus_target_employee_id
  return head(:unprocessable_entity) unless target_id

  rep = Employee.find_by(id: target_id)
  review_date = review_date_param_for(conversation)
  covered_ids = Array(conversation.daily_focus_covered_employee_ids).map(&:to_i)
  conversation.release_processing_lock! if conversation.processing_by_id.present?
  conversation.destroy!

  respond_to do |format|
    format.turbo_stream do
      ag = (review_date == Date.current)
      covered_by_id = Employee.where(id: covered_ids).index_by(&:id)
      covered_streams = covered_ids.filter_map do |cid|
        cov = covered_by_id[cid]
        next unless cov

        turbo_stream.replace(
          "daily-focus-row-#{cov.id}",
          partial: 'daily_focus/review_row',
          locals: review_row_locals(cov, :missing, nil, allow_generate_missing: ag, for_date: review_date)
        )
      end
      render turbo_stream: [
        turbo_stream.replace(
          "daily-focus-row-#{rep.id}",
          partial: 'daily_focus/review_row',
          locals: review_row_locals(
            rep,
            :missing,
            nil,
            allow_generate_missing: ag,
            for_date: review_date
          )
        ),
        *covered_streams,
        turbo_stream.replace("daily-focus-stats", partial: 'daily_focus/stats_cards', locals: stats_card_locals(for_date: review_date))
      ]
    end
    format.html { redirect_to review_daily_focus_path(date: review_date.iso8601), notice: "Daily focus rejected for #{rep.full_name}" }
  end
end

#reviewObject



43
44
45
# File 'app/controllers/daily_focus_controller.rb', line 43

def review
  build_review_data
end

#summaryObject

Render the Daily Focus summary for the current date.

Computes daily focus counts for Date.current and renders the Crm::DailyFocusSummaryComponent
with processing, pending, approved, and missing counts.



29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'app/controllers/daily_focus_controller.rb', line 29

def summary
  counts = compute_focus_counts(Date.current)
  respond_to do |format|
    format.html do
      render Crm::DailyFocusSummaryComponent.new(
        processing_count: counts[:processing],
        pending_count: counts[:pending],
        approved_count: counts[:approved],
        missing_count: counts[:missing]
      )
    end
  end
end

#widget_indexObject

Render the Daily Focus widget for the target employee.
Ensures the target employee is loaded, sets @conversation to that employee's daily focus conversation, and renders the Crm::DailyFocusWidgetComponent as HTML.



16
17
18
19
20
21
22
# File 'app/controllers/daily_focus_controller.rb', line 16

def widget_index
  load_employee
  @conversation = find_daily_focus_conversation
  respond_to do |format|
    format.html { render Crm::DailyFocusWidgetComponent.new(employee: target_employee, conversation: @conversation) }
  end
end