Class: DailyFocusController
- Inherits:
-
CrmController
- Object
- ActionController::Base
- ApplicationController
- CrmController
- DailyFocusController
- Includes:
- DailyFocusAudiencesHelper
- Defined in:
- app/controllers/daily_focus_controller.rb
Overview
Controller: daily focus.
Constant Summary collapse
- DAILY_FOCUS_TOOL_SERVICES =
Daily focus tool services.
DailyFocus::TOOL_SERVICE_KEYS
- DAILY_FOCUS_TOOL_HANDLES =
Daily focus tool handles.
%w[sales-management app-db support-cases content-search].freeze
- WORK_DAY_INDICATOR_UNSET =
Work day indicator unset.
Object.new.freeze
Constants included from DailyFocusAudiencesHelper
DailyFocusAudiencesHelper::AUDIENCES, DailyFocusAudiencesHelper::AUDIENCE_LABELS
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
Instance Method Summary collapse
-
#approve ⇒ Object
Approve a daily focus briefing and mark it as approved for its target employee.
-
#edit_coaching_note ⇒ Object
Renders the coaching-note editor offcanvas for one rep into the layout's
#modalturbo-frame. -
#generate ⇒ Object
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.
-
#generate_with_instructions ⇒ Object
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 leadership, and redirects the user to the assistant view for that conversation.
-
#reject ⇒ Object
Deletes a daily focus briefing (pending, processing, or already-approved), removes its processing lock, destroys the associated conversation, and resets the review UI so it can be regenerated.
-
#review ⇒ void
Renders the daily focus review page, building the review data via
build_review_data. -
#summary ⇒ Object
Render the Daily Focus summary for the current date.
-
#update_coaching_note ⇒ Object
Upserts (or clears) a rep's standing coaching note.
-
#widget_index ⇒ Object
Render the Daily Focus widget for the target employee.
Methods included from DailyFocusAudiencesHelper
#can_manage_audience?, #daily_focus_audience_label, #manageable_daily_focus_audiences, #sales_daily_focus_manager?, #technical_support_manager?
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, #init_status_job_collector, #initialize_crm_lazy_chunks, #persist_enqueued_status_jobs, #record_not_found, #redirect_to_job_or_fallback, #render_edit_action, #set_context, #set_download_path, #stash_file_for_temp_download, #sync_admin_presence_cookie, #touch_employee_last_seen
Methods inherited from ApplicationController
#account_impersonated?, #add_to_flash, #after_sign_in_path_for, #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?, #skip_layout_for_turbo_frame?, #stamp_impersonation_context, #tab_frame_breakout_request?, #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, #registration_lead_type, #track_event
Methods included from Controllers::DeviceDetection
Methods included from Controllers::SubdomainDetection
#is_crm_request?, #is_www_request?, #json_request?
Methods included from Controllers::TurboSafeRedirect
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, #render_unpermitted_parameters, #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_edge_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!, #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 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?
Instance Method Details
#approve ⇒ Object
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.
Reads params[:id] (an AssistantConversation id, Integer or String).
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'app/controllers/daily_focus_controller.rb', line 72 def approve conversation = AssistantConversation.find(params.expect(:id)) target_id = conversation.daily_focus_target_employee_id return head(:unprocessable_content) unless target_id rep = Employee.find(target_id) audience = conversation.daily_focus_audience.presence || 'sales' return access_denied(nil) unless can_manage_audience?(audience) 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 InternalReportsMailer.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.update("daily-focus-stats", partial: 'daily_focus/stats_cards', locals: stats_card_locals(for_date: rd, audience: audience)) ] end format.html { redirect_to review_daily_focus_path(date: review_date_param_for(conversation).iso8601, audience: (audience if audience == 'support')), notice: "Daily focus approved for #{rep.full_name}" } end end |
#edit_coaching_note ⇒ Object
Renders the coaching-note editor offcanvas for one rep into the layout's
#modal turbo-frame. GET, turbo-frame only (mirrors edit_watch_list).
282 283 284 285 286 287 288 289 290 |
# File 'app/controllers/daily_focus_controller.rb', line 282 def edit_coaching_note return redirect_to(review_daily_focus_path(date: params[:date].presence)) unless turbo_frame_request? @employee = Employee.find(params.expect(:employee_id)) return access_denied(nil) unless can_manage_audience?(DailyFocus.audience_for(@employee)) @note = @employee.daily_focus_coaching_note render partial: 'coaching_note_offcanvas', layout: false end |
#generate ⇒ Object
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
DailyFocusAnalysisRepWorkerto 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.
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 |
# File 'app/controllers/daily_focus_controller.rb', line 197 def generate employee = Employee.find(params.expect(:employee_id)) audience = DailyFocus.audience_for(employee) return access_denied(nil) unless can_manage_audience?(audience) cleanup_stale_daily_focus(employee, audience: audience) 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', 'daily_focus_audience' => audience, 'conversation_type' => 'daily_briefing', 'tool_services' => DailyFocus::TOOL_SERVICE_KEYS )) DailyFocus::ConversationSharing.share_with_leadership!( conversation, shared_by: current_user, audience: audience ) DailyFocusAnalysisRepWorker.perform_async(employee.id, conversation.id, [], 'user') 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.update("daily-focus-stats", partial: 'daily_focus/stats_cards', locals: stats_card_locals(for_date: Date.current, audience: audience)) ] end format.html { redirect_to review_daily_focus_path(date: Date.current.iso8601, audience: (audience if audience == 'support')), notice: "Daily focus generation queued for #{employee.full_name}" } end end |
#generate_with_instructions ⇒ Object
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 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'),daily_focus_audience,
conversation_type('daily_briefing'), andtool_services(the configured daily-focus services). - Shares the conversation with audience leadership (sales or tech support managers).
- Stores a long instructional prefill in
conversation.draft_prefilland default tool handles in
conversation.default_tool_handlesso they survive any redirect and a refresh. - Redirects to the assistant view for the newly created conversation.
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 |
# File 'app/controllers/daily_focus_controller.rb', line 250 def generate_with_instructions employee = Employee.find(params.expect(:employee_id)) audience = DailyFocus.audience_for(employee) return access_denied(nil) unless can_manage_audience?(audience) cleanup_stale_daily_focus(employee, audience: audience) 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', 'daily_focus_audience' => audience, '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_leadership!( conversation, shared_by: current_user, audience: audience ) redirect_to assistant_path(conversation) end |
#reject ⇒ Object
Deletes a daily focus briefing (pending, processing, or already-approved), removes its processing lock, destroys the associated conversation, and resets the review UI so it can be regenerated.
Finds the AssistantConversation by params[:id], releases its processing lock if held, and destroys it. Responds with HTTP 422 Unprocessable Entity unless the conversation is a daily briefing tied to a known rep.
Pending/processing briefings carry daily_focus_target_employee_id; approval strips it (and sets user_id to the rep), so the rep is resolved from either.
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.
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 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 |
# File 'app/controllers/daily_focus_controller.rb', line 132 def reject conversation = AssistantConversation.find(params.expect(:id)) return head(:unprocessable_content) unless conversation.conversation_type == 'daily_briefing' # Approval strips BOTH daily_focus_target_employee_id and daily_focus_status and sets # user_id to the rep. Pending/processing/failed keep a status and a manager user_id, # so only fall back to user_id in the stripped (approved) state — otherwise a bad-data # non-approved record would misattribute to the manager instead of returning 422. target_id = conversation.daily_focus_target_employee_id || (conversation.daily_focus_status.nil? ? conversation.user_id : nil) rep = Employee.find_by(id: target_id) return head(:unprocessable_content) unless rep # Persisted audience only (legacy nil = sales) — re-deriving from the rep's # CURRENT roles would misroute a legacy sales briefing after a role change. audience = conversation.daily_focus_audience.presence || 'sales' return access_denied(nil) unless can_manage_audience?(audience) 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.update("daily-focus-stats", partial: 'daily_focus/stats_cards', locals: stats_card_locals(for_date: review_date, audience: audience)) ] end format.html { redirect_to review_daily_focus_path(date: review_date.iso8601, audience: (audience if audience == 'support')), notice: "Daily focus rejected for #{rep.full_name}" } end end |
#review ⇒ void
This method returns an undefined value.
Renders the daily focus review page, building the review data via build_review_data.
60 61 62 |
# File 'app/controllers/daily_focus_controller.rb', line 60 def review build_review_data end |
#summary ⇒ Object
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.
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'app/controllers/daily_focus_controller.rb', line 38 def summary # One card per population, so a tech manager's card counts tech reps. It # used to take the audience-less default and report sales to everyone. audience = params[:audience].to_s == 'support' ? 'support' : 'sales' return access_denied(nil) unless can_manage_audience?(audience) counts = compute_focus_counts(Date.current, audience: audience) 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], audience: audience ) end end end |
#update_coaching_note ⇒ Object
Upserts (or clears) a rep's standing coaching note. A blank body — or the
explicit Clear button (clear param) — deletes the note. Responds by
closing the offcanvas and swapping the rep's coaching-note button so its
"note present" state updates without a full page reload.
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 |
# File 'app/controllers/daily_focus_controller.rb', line 297 def update_coaching_note employee = Employee.find(params.expect(:employee_id)) return access_denied(nil) unless can_manage_audience?(DailyFocus.audience_for(employee)) note = employee.daily_focus_coaching_note if params[:clear].present? || params[:body].blank? note&.destroy note = nil else note ||= employee.build_daily_focus_coaching_note note.update!(body: params[:body]) end respond_to do |format| format.turbo_stream do render turbo_stream: [ turbo_stream.bs_offcanvas_hide('coaching-note-offcanvas'), turbo_stream.replace( "coaching-note-btn-#{employee.id}", partial: 'daily_focus/coaching_note_button', locals: { rep: employee, note: note } ) ] end format.html { redirect_to review_daily_focus_path(date: params[:date].presence), notice: 'Coaching note saved.' } end end |
#widget_index ⇒ Object
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.
25 26 27 28 29 30 31 |
# File 'app/controllers/daily_focus_controller.rb', line 25 def load_employee @conversation = find_daily_focus_conversation respond_to do |format| format.html { render Crm::DailyFocusWidgetComponent.new(employee: target_employee, conversation: @conversation) } end end |