Class: Crm::RetailersController
- Inherits:
-
CrmController
- Object
- ActionController::Base
- ApplicationController
- CrmController
- Crm::RetailersController
- Defined in:
- app/controllers/crm/retailers_controller.rb
Overview
Per-retailer operational dashboards. The index mirrors the daily
retailer-compliance email (Retailer::DailyComplianceReport) as an
at-a-glance grid; each :id (a Catalog) gets a dashboard with a health gauge,
trends (from CatalogDataPoint history), action items, a sales summary, and
probe health, plus quick links into the catalog, customers, listing issues,
EDI, and webhooks.
Constant Summary collapse
- TAB_ACTIONS =
Lazy-tab actions guarded by require_in_pane_tab_frames (frame-only).
%i[tab_overview tab_trends tab_action_items tab_sales tab_probes tab_listings].freeze
- INDEX_CACHE_KEY =
Short-TTL cache of the index's report rows (the compliance counts move at
most daily). Exposed so tests can clear it between runs. 'crm/retailers/dashboard_rows'
Constants included from Controllers::AmazonProductsGrid
Controllers::AmazonProductsGrid::AMAZON_SELLER_IDS, Controllers::AmazonProductsGrid::GRID_FILTER_PARAMS
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
-
#analyze ⇒ void
POST: kick off a Sunny AI analysis of this catalog (health, sales, errors, onboarding, assortment gaps, advertising).
-
#index ⇒ void
At-a-glance grid of every retailer in the compliance scope, grouped by region, with health and 30-day sales alongside the email's counts.
-
#show ⇒ void
One retailer's dashboard: heading + tab nav; every section lazy-loads through its tab_* frame (Overview preselected).
-
#tab_action_items ⇒ void
Lazy frame: open listing issues bucketed by code (buy_box_lost split into out-of-stock-driven vs genuine competition losses) + top MAP offenders.
-
#tab_listings ⇒ void
Lazy frame: the Amazon per-item operations grid (buy box, issues, price, competitive price, freshness) scoped to this catalog — the same engine the standalone /amazon_catalog_items page ran, consolidated onto the retailer dashboard.
-
#tab_overview ⇒ void
Lazy frame (default tab): health gauge, compliance metrics, Amazon ops tiles with deep-filter links into the Listings frame, AI analysis, and the pre-made catalog queries.
-
#tab_probes ⇒ void
Lazy frame: probe status breakdown over the compliance lookback window.
-
#tab_sales ⇒ void
Lazy frame: sales_orders count + revenue trend for this retailer.
-
#tab_trends ⇒ void
Lazy frame: compliance-metric trends from CatalogDataPoint history.
Methods included from Controllers::RequireTurboFrame
require_in_pane_tab_frames, require_turbo_frame
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
#analyze ⇒ void
This method returns an undefined value.
POST: kick off a Sunny AI analysis of this catalog (health, sales,
errors, onboarding, assortment gaps, advertising). One run at a time
per catalog; each run is a linked AssistantConversation so past
analyses stay browsable from the dashboard. The lookup-and-create runs
inside a catalog-keyed advisory lock so concurrent POSTs can't both
start a run; the worker re-verifies under the same lock before doing
the expensive chat call.
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'app/controllers/crm/retailers_controller.rb', line 148 def analyze # The loser of a concurrent pair just renders the panel — the winner's # row is already visible as processing. AssistantConversation.with_advisory_lock_result("retailer_analysis:#{@catalog.id}", timeout_seconds: 0) do processing_analysis || start_analysis end respond_to do |format| format.turbo_stream do render turbo_stream: turbo_stream.replace( ActionView::RecordIdentifier.dom_id(@catalog, :analysis), partial: 'crm/retailers/analysis', locals: { catalog: @catalog, analyses: AssistantConversation.retailer_analyses_for(@catalog.id).limit(6).to_a } ) end format.html { redirect_to retailer_path(@catalog) } end end |
#index ⇒ void
This method returns an undefined value.
At-a-glance grid of every retailer in the compliance scope, grouped by
region, with health and 30-day sales alongside the email's counts.
30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'app/controllers/crm/retailers_controller.rb', line 30 def index # The compliance counts change at most daily (probe/sync/report cadence), # so serve the recursive report SQL from a short-TTL cache rather than # re-running it on every page view. rows = Rails.cache.fetch(INDEX_CACHE_KEY, expires_in: 10.minutes) do Retailer::DailyComplianceReport.for_dashboard.rows end @report = Retailer::DailyComplianceReport.new(rows:) @rows_by_region = @report.rows_by_region @totals = @report.totals @flagged = @report.flagged_rows.sort_by { |r| -action_weight(r) } @sales = sales_by_catalog(@report.rows.pluck(:catalog_id), since: 30.days.ago) end |
#show ⇒ void
This method returns an undefined value.
One retailer's dashboard: heading + tab nav; every section lazy-loads
through its tab_* frame (Overview preselected).
47 48 49 50 51 |
# File 'app/controllers/crm/retailers_controller.rb', line 47 def show @open_issue_count = ListingIssue.open.joins(:catalog_item) .where(catalog_items: { catalog_id: @catalog.id }).count @amazon_tiles = amazon_tiles(@catalog) if @catalog.id.in?(CatalogConstants::AMAZON_SELLER_IDS) end |
#tab_action_items ⇒ void
This method returns an undefined value.
Lazy frame: open listing issues bucketed by code (buy_box_lost split into
out-of-stock-driven vs genuine competition losses) + top MAP offenders.
Counts come from SQL; only the handful of sample rows per bucket is
materialized.
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'app/controllers/crm/retailers_controller.rb', line 76 def tab_action_items scoped = ListingIssue.open.joins(:catalog_item).where(catalog_items: { catalog_id: @catalog.id }) counts = scoped.group(:code).count severities = scoped.group(:code).minimum(:severity) titles = scoped.group(:code).minimum(:title) oos_ci_ids = AmazonCatalogItemFlag.active.where(flag_type: 'out_of_stock') .joins(:catalog_item) .where(catalog_items: { catalog_id: @catalog.id }) .select(:catalog_item_id) @issue_buckets = counts.sort_by { |_, count| -count }.map do |code, count| samples = scoped.where(code:).order(last_seen_at: :desc) .includes(catalog_item: [:catalog, { store_item: :item }]).limit(5) bucket = { code:, severity: severities[code], title: titles[code], count:, issues: samples } if code == 'buy_box_lost' oos_count = ListingIssue.open.where(catalog_item_id: oos_ci_ids, code: 'buy_box_lost').count bucket[:out_of_stock_count] = oos_count bucket[:competition_count] = count - oos_count end bucket end @map_offenders = map_offenders(@catalog) render_tab end |
#tab_listings ⇒ void
This method returns an undefined value.
Lazy frame: the Amazon per-item operations grid (buy box, issues, price,
competitive price, freshness) scoped to this catalog — the same engine
the standalone /amazon_catalog_items page ran, consolidated onto the
retailer dashboard. Only meaningful for Amazon seller catalogs (the
dashboard only links the frame for those).
131 132 133 134 135 136 137 138 |
# File 'app/controllers/crm/retailers_controller.rb', line 131 def tab_listings return head :not_found unless @catalog.id.in?(CatalogConstants::AMAZON_SELLER_IDS) # NB: setup_amazon_products_grid re-derives @catalog from the selected # catalogs — the same record set_catalog loaded. setup_amazon_products_grid([@catalog.id]) render_tab end |
#tab_overview ⇒ void
This method returns an undefined value.
Lazy frame (default tab): health gauge, compliance metrics, Amazon ops
tiles with deep-filter links into the Listings frame, AI analysis, and
the pre-made catalog queries.
57 58 59 60 61 62 |
# File 'app/controllers/crm/retailers_controller.rb', line 57 def tab_overview @customers = @catalog.customers.to_a.sort_by { |c| c.name.to_s } @analyses = AssistantConversation.retailer_analyses_for(@catalog.id).limit(6).to_a @amazon_tiles = amazon_tiles(@catalog) if @catalog.id.in?(CatalogConstants::AMAZON_SELLER_IDS) render_tab end |
#tab_probes ⇒ void
This method returns an undefined value.
Lazy frame: probe status breakdown over the compliance lookback window.
117 118 119 120 121 122 123 |
# File 'app/controllers/crm/retailers_controller.rb', line 117 def tab_probes @probe_counts = probe_status_breakdown(@catalog) @last_probe_at = CatalogItemRetailerProbe.joins(:catalog_item) .where(catalog_items: { catalog_id: @catalog.id }) .maximum(:created_at) render_tab end |
#tab_sales ⇒ void
This method returns an undefined value.
Lazy frame: sales_orders count + revenue trend for this retailer.
103 104 105 106 107 108 109 110 111 112 113 |
# File 'app/controllers/crm/retailers_controller.rb', line 103 def tab_sales @monthly = @catalog.orders.sales_orders .where(created_at: 12.months.ago.beginning_of_month..) .reorder(nil) .group_by_month('orders.created_at', last: 12, format: '%b %Y') @orders_by_month = @monthly.count @revenue_by_month = @monthly.sum(:total) @recent_orders = @catalog.orders.sales_orders.order(created_at: :desc) .includes(:customer).limit(15) render_tab end |
#tab_trends ⇒ void
This method returns an undefined value.
Lazy frame: compliance-metric trends from CatalogDataPoint history.
66 67 68 69 |
# File 'app/controllers/crm/retailers_controller.rb', line 66 def tab_trends @series = trend_series(@catalog) render_tab end |