Class: Crm::ShowcasesController

Inherits:
CrmController show all
Includes:
Controllers::Workflowable
Defined in:
app/controllers/crm/showcases_controller.rb

Overview

CRUD, dashboard metrics, and image-management actions for Showcase
marketing pages.

Constant Summary

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

Methods included from Controllers::Workflowable

#render_workflow_error_stream, #render_workflow_success_stream, #workflow_action, #workflow_action_complete

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

#device_detector, #is_ie?

Methods included from Controllers::SubdomainDetection

#is_crm_request?, #is_www_request?, #json_request?

Methods included from Controllers::TurboSafeRedirect

#redirect_to

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

#check_post_conflictsHash

POST /showcases/check_post_conflicts
Params: post_ids[] (array of integers), exclude_id (custom_slug of current showcase, optional)

Returns:

  • (Hash)

    { warnings: [ { post_id, post_subject, showcase_slug, showcase_name, url } ] }



387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'app/controllers/crm/showcases_controller.rb', line 387

def check_post_conflicts
  ids = Array(params[:post_ids]).map(&:to_i).uniq.compact
  exclude_slug = params[:exclude_id].to_s.presence
  return render json: { warnings: [] } if ids.blank?

  scope = Showcase.published
  scope = scope.where.not(custom_slug: exclude_slug) if exclude_slug.present?

  # Find showcases that reference any of the given post_ids
  matches = scope
            .where('array_remove(post_ids, NULL) && ARRAY[?]::integer[]', ids)
            .select(:custom_slug, :name, :post_ids)

  warnings = []
  if matches.any?
    # Preload posts to map id -> subject for message clarity
    subjects = Post.where(id: ids).pluck(:id, :subject).to_h
    matches.each do |sc|
      overlapping = Array(sc.post_ids).compact & ids
      overlapping.each do |pid|
        warnings << {
          post_id: pid,
          post_subject: subjects[pid] || "Post ##{pid}",
          showcase_slug: sc.custom_slug,
          showcase_name: sc.name,
          url: Rails.application.routes.url_helpers.showcase_url(sc.custom_slug, host: request.host_with_port)
        }
      end
    end
  end

  render json: { warnings: warnings }
end

#createvoid

This method returns an undefined value.

Creates a showcase in the draft state.



180
181
182
183
184
185
186
187
188
# File 'app/controllers/crm/showcases_controller.rb', line 180

def create
  @showcase = Showcase.new(showcase_params)
  @showcase.state = 'draft'
  if @showcase.save
    redirect_to showcase_path(@showcase), notice: 'Showcase was successfully created.'
  else
    render :new, status: :unprocessable_content
  end
end

#destroyvoid

This method returns an undefined value.

Deletes the showcase loaded by #set_showcase.



204
205
206
207
# File 'app/controllers/crm/showcases_controller.rb', line 204

def destroy
  @showcase.destroy
  redirect_to showcases_url, notice: 'Showcase was successfully destroyed.'
end

#editvoid

This method returns an undefined value.

Loads the showcase to edit via #set_showcase.



175
# File 'app/controllers/crm/showcases_controller.rb', line 175

def edit; end

#edit_imagesvoid

This method returns an undefined value.

Renders the image-management screen for the showcase loaded by
#set_showcase.



220
221
222
# File 'app/controllers/crm/showcases_controller.rb', line 220

def edit_images
  # For parity, we could load assets; no room configuration joins
end

#flush_cachevoid

This method returns an undefined value.

Purges the edge cache for the showcase loaded by #set_showcase.



377
378
379
380
381
382
# File 'app/controllers/crm/showcases_controller.rb', line 377

def flush_cache
  authorize!(:update, Showcase)
  @showcase.touch
  @showcase.purge_edge_cache
  redirect_to showcase_path(@showcase), notice: 'Cache purge has been queued, this can take a couple minutes.'
end

#flush_edge_cachevoid

This method returns an undefined value.

Touches and flushes the edge cache for every published showcase.



367
368
369
370
371
372
# File 'app/controllers/crm/showcases_controller.rb', line 367

def flush_edge_cache
  authorize!(:update, Showcase)
  Showcase.published.touch_all
  Showcase.flush_edge_cache
  redirect_to showcases_path, notice: 'Cache refreshed for all showcases, this can take a couple minutes.'
end

#get_data_from_roomObject

POST /showcases/get_data_from_room



343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'app/controllers/crm/showcases_controller.rb', line 343

def get_data_from_room
  rc = RoomConfiguration.find(params[:room_id])
  return head :not_found unless rc

  room_size = rc.insulation_surface || rc.installation_sqft
  render json: {
    room_type: rc.room_type.id,
    size: "#{room_size} sq.ft.",
    volts: rc.installation_plan_voltage_id,
    product_line_id: rc.heating_system_product_line_id,
    customer_id: rc.customer&.id,
    customer_name: rc.customer&.full_name,
    watts: rc.calculate_total_watts,
    amps: rc.calculate_total_amps,
    floor_type_id: rc.floor_type_id,
    sub_floor_type_id: rc.sub_floor_type_id,
    operating_cost: rc.calculate_operating_cost_rate_using_hep,
    quote_ids: rc.quote_ids
  }
end

#import_images_from_roomObject

POST /showcases/:id/import_images_from_room



320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'app/controllers/crm/showcases_controller.rb', line 320

def import_images_from_room
  redirect_to edit_images_showcase_path(@showcase), alert: 'No room configuration selected.' and return if params[:room_configuration_id].blank? && @showcase.room_configuration_id.blank?

  room_id = params[:room_configuration_id].presence || @showcase.room_configuration_id
  room_config = RoomConfiguration.find_by(id: room_id)
  redirect_to edit_images_showcase_path(@showcase), alert: 'Room configuration not found.' and return unless room_config

  # Import room layout, installation plan and photos similar to FloorPlanDisplay
  imported = 0
  if room_config.installation_plan_image.present?
    imported += 1 if create_image_link_from_upload(room_config.installation_plan_image, room_config, 'Installation Plan')
  end
  if room_config.room_layout_image.present?
    imported += 1 if create_image_link_from_upload(room_config.room_layout_image, room_config, 'Room Layout')
  end
  room_config.uploads.in_category('photo').each do |upload|
    imported += 1 if create_image_link_from_upload(upload, room_config, 'Photo')
  end

  redirect_to edit_images_showcase_path(@showcase), notice: (imported.positive? ? 'Images successfully imported from room configuration.' : 'No images were imported.')
end

#indexvoid

This method returns an undefined value.

Lists showcases with dashboard completeness metrics (published/draft
counts, percentage-complete breakdowns for SEO/description/image
fields) and Ransack/quick-filter search.



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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
96
97
98
99
100
101
102
103
104
105
106
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
151
152
153
154
155
156
157
# File 'app/controllers/crm/showcases_controller.rb', line 15

def index
  @q = Showcase.all.ransack(params[:q])
  @q.sorts = 'created_at DESC' if @q.sorts.blank?
  @showcases = @q.result.includes(:customer, showcase_digital_assets: :digital_asset)

  # Fan out 16 aggregates concurrently via Rails 7.1 async_count.
  # Each Showcase.<scope>.async_count returns an ActiveRecord::Promise that
  # materializes when the value is read in transform_values(&:value).
  promises = {
    total:                              Showcase.async_count,
    published:                          Showcase.published.async_count,
    draft:                              Showcase.draft.async_count,
    archived:                           Showcase.archived.async_count,
    with_breadcrumbs:                   Showcase.where('COALESCE(array_length(breadcrumbs, 1), 0) > 0').async_count,
    with_room_types:                    Showcase.where('COALESCE(array_length(room_types, 1), 0) > 0').async_count,
    with_faqs:                          Showcase.joins(:faqs).distinct.async_count,
    # array_remove: don't count NULL placeholders as elements
    with_quotes:                        Showcase.where('COALESCE(array_length(array_remove(quote_ids, NULL), 1), 0) > 0').async_count,
    with_posts:                         Showcase.where('COALESCE(array_length(array_remove(post_ids, NULL), 1), 0) > 0').async_count,
    with_description:                   Showcase.where("COALESCE(NULLIF(BTRIM(description), ''), NULL) IS NOT NULL").async_count,
    with_seo_description:               Showcase.where("COALESCE(NULLIF(BTRIM(seo_description), ''), NULL) IS NOT NULL").async_count,
    with_seo_title:                     Showcase.where("COALESCE(NULLIF(BTRIM(seo_title), ''), NULL) IS NOT NULL").async_count,
    with_short_description:             Showcase.where("COALESCE(NULLIF(BTRIM(short_description), ''), NULL) IS NOT NULL").async_count,
    without_operating_cost_explanation: Showcase.where("COALESCE(NULLIF(BTRIM(operating_cost_explanation), ''), '') = ''").async_count,
    without_images:                     Showcase.where.missing(:showcase_digital_assets).async_count,
    without_display_options:            Showcase.where('COALESCE(jsonb_array_length(enabled_buttons), 0) = 0').async_count
  }
  counts = promises.transform_values(&:value)

  @total_showcases = counts[:total]
  @published_showcases = counts[:published]
  @draft_showcases = counts[:draft]
  @archived_showcases = counts[:archived]

  @published_percentage = percentage_of(@published_showcases, @total_showcases)
  @draft_percentage = percentage_of(@draft_showcases, @total_showcases)
  @archived_percentage = percentage_of(@archived_showcases, @total_showcases)

  @with_breadcrumbs = counts[:with_breadcrumbs]
  @without_breadcrumbs = @total_showcases - @with_breadcrumbs
  @breadcrumb_percentage = percentage_of(@with_breadcrumbs, @total_showcases)

  @with_room_types = counts[:with_room_types]
  @without_room_types = @total_showcases - @with_room_types
  @room_types_percentage = percentage_of(@with_room_types, @total_showcases)

  @with_faqs = counts[:with_faqs]
  @without_faqs = @total_showcases - @with_faqs
  @faqs_percentage = percentage_of(@with_faqs, @total_showcases)

  @with_quotes = counts[:with_quotes]
  @without_quotes = @total_showcases - @with_quotes
  @quotes_percentage = percentage_of(@with_quotes, @total_showcases)

  @with_posts = counts[:with_posts]
  @without_posts = @total_showcases - @with_posts
  @posts_percentage = percentage_of(@with_posts, @total_showcases)

  @with_description = counts[:with_description]
  @without_description = @total_showcases - @with_description
  @description_percentage = percentage_of(@with_description, @total_showcases)

  @with_seo_description = counts[:with_seo_description]
  @without_seo_description = @total_showcases - @with_seo_description
  @seo_description_percentage = percentage_of(@with_seo_description, @total_showcases)

  @with_seo_title = counts[:with_seo_title]
  @without_seo_title = @total_showcases - @with_seo_title
  @seo_title_percentage = percentage_of(@with_seo_title, @total_showcases)

  @with_short_description = counts[:with_short_description]
  @without_short_description = @total_showcases - @with_short_description
  @short_description_percentage = percentage_of(@with_short_description, @total_showcases)

  @without_operating_cost_explanation = counts[:without_operating_cost_explanation]
  @with_operating_cost_explanation = @total_showcases - @without_operating_cost_explanation
  @without_operating_cost_explanation_percentage = percentage_of(@without_operating_cost_explanation, @total_showcases)
  @with_operating_cost_explanation_percentage = percentage_of(@with_operating_cost_explanation, @total_showcases)

  @without_images = counts[:without_images]
  @with_images = @total_showcases - @without_images
  @without_images_percentage = percentage_of(@without_images, @total_showcases)

  @without_display_options = counts[:without_display_options]
  @with_display_options = @total_showcases - @without_display_options
  @without_display_options_percentage = percentage_of(@without_display_options, @total_showcases)

  # Extra filters - handle both '1' (with) and '0' (without) cases
  @showcases = apply_text_field_filter(@showcases, :with_description, 'description')
  @showcases = apply_text_field_filter(@showcases, :with_seo_description, 'seo_description')
  @showcases = apply_text_field_filter(@showcases, :with_seo_title, 'seo_title')
  @showcases = apply_text_field_filter(@showcases, :with_short_description, 'short_description')
  @showcases = apply_array_field_filter(@showcases, :with_quotes, 'quote_ids')
  @showcases = apply_array_field_filter(@showcases, :with_posts, 'post_ids')
  @showcases = apply_association_filter(@showcases, :with_faqs, :faqs)
  @showcases = apply_association_filter(@showcases, :without_images, :showcase_digital_assets, inverted: true)

  @showcases = @showcases.where("COALESCE(NULLIF(BTRIM(operating_cost_explanation), ''), '') = ''") if params[:without_operating_cost_explanation] == '1'
  @showcases = @showcases.where("COALESCE(NULLIF(BTRIM(operating_cost_explanation), ''), NULL) IS NOT NULL") if params[:with_operating_cost_explanation] == '1'
  @showcases = @showcases.where('COALESCE(jsonb_array_length(enabled_buttons), 0) = 0') if params[:without_display_options_checked] == '1'
  # Breadcrumbs quick filter
  if params.key?(:with_breadcrumbs)
    if params[:with_breadcrumbs].to_s == '1'
      @showcases = @showcases.where('COALESCE(array_length(breadcrumbs, 1), 0) > 0')
    elsif params[:with_breadcrumbs].to_s == '0'
      @showcases = @showcases.where('COALESCE(array_length(breadcrumbs, 1), 0) = 0')
    end
  end

  # Ensure scope-based filters apply even if Ransack skips custom scopes
  if params[:q].is_a?(ActionController::Parameters) || params[:q].is_a?(Hash)
    qparams = params[:q]
    if qparams.key?(:by_product_line_id)
      vals = Array(qparams[:by_product_line_id])
      @showcases = @showcases.by_product_line_id(vals) if vals.any?(&:present?)
    end
    if qparams.key?(:by_quote_id)
      vals = Array(qparams[:by_quote_id])
      @showcases = @showcases.by_quote_id(vals) if vals.any?(&:present?)
    end
    if qparams.key?(:by_sku)
      val = qparams[:by_sku].to_s
      @showcases = @showcases.by_sku(val) if val.present?
    end
    if qparams.key?(:tags_include)
      vals = Array(qparams[:tags_include])
      @showcases = @showcases.tags_include(vals) if vals.any?(&:present?)
    end
    if qparams.key?(:room_types_include)
      vals = Array(qparams[:room_types_include])
      @showcases = @showcases.room_types_include(vals) if vals.any?(&:present?)
    end
  end

  respond_to do |format|
    format.html do
      @pagy, @showcases = pagy(@showcases)
    end
    format.csv do
      send_data generate_csv(@showcases), filename: "showcases-#{Date.current}.csv"
    end
  end
end

POST /showcases/:id/link_images



272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'app/controllers/crm/showcases_controller.rb', line 272

def link_images
  image_ids = Array(params[:image_ids]).map(&:to_i).uniq

  redirect_to edit_images_showcase_path(@showcase), alert: 'No images selected.' and return if image_ids.blank?

  position_start = @showcase.showcase_digital_assets.maximum(:position).to_i

  created = 0
  image_ids.each_with_index do |image_id, idx|
    exists = @showcase.showcase_digital_assets.where(digital_asset_id: image_id).exists?
    next if exists

    @showcase.showcase_digital_assets.create!(
      digital_asset_id: image_id,
      position: position_start + idx + 1
    )
    created += 1
  end

  notice = created.positive? ? 'Images added to showcase.' : 'Selected images were already linked.'
  redirect_to edit_images_showcase_path(@showcase), notice: notice
end

POST /showcases/:id/link_videos



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'app/controllers/crm/showcases_controller.rb', line 296

def link_videos
  video_ids = Array(params[:video_ids]).map(&:to_i).uniq

  redirect_to edit_images_showcase_path(@showcase), alert: 'No videos selected.' and return if video_ids.blank?

  position_start = @showcase.showcase_digital_assets.maximum(:position).to_i

  created = 0
  video_ids.each_with_index do |video_id, idx|
    exists = @showcase.showcase_digital_assets.where(digital_asset_id: video_id).exists?
    next if exists

    @showcase.showcase_digital_assets.create!(
      digital_asset_id: video_id,
      position: position_start + idx + 1
    )
    created += 1
  end

  notice = created.positive? ? "#{created} video(s) added to showcase." : 'Selected videos were already linked.'
  redirect_to edit_images_showcase_path(@showcase), notice: notice
end

#newvoid

This method returns an undefined value.

Builds a new showcase with the default enabled buttons.



167
168
169
170
# File 'app/controllers/crm/showcases_controller.rb', line 167

def new
  @showcase = Showcase.new
  @showcase.enabled_buttons = %w[button_design_room button_floor_heating_quote button_customize_floor_plan]
end

#previewvoid

This method returns an undefined value.

Renders the public-facing showcase page inline, for staff preview.



212
213
214
# File 'app/controllers/crm/showcases_controller.rb', line 212

def preview
  render template: 'www/showcases/show', layout: 'www/cms_page'
end

#set_main_imageObject

POST /showcases/:id/set_main_image



247
248
249
250
251
252
253
254
255
256
257
# File 'app/controllers/crm/showcases_controller.rb', line 247

def set_main_image
  da_id = params[:digital_asset_id].to_i
  join = @showcase.showcase_digital_assets.find_by(digital_asset_id: da_id)
  if join
    @showcase.showcase_digital_assets.excluding(join).update_all(is_main: false)
    join.update!(is_main: true)
    redirect_to edit_images_showcase_path(@showcase), notice: 'Main image updated.'
  else
    redirect_to edit_images_showcase_path(@showcase), alert: 'Image not found.'
  end
end

#showvoid

This method returns an undefined value.

Shows the showcase loaded by #set_showcase.



162
# File 'app/controllers/crm/showcases_controller.rb', line 162

def show; end

#sort_imagesvoid

This method returns an undefined value.

Bulk-updates the position of the showcase's linked images from an
ordered list of ids.



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'app/controllers/crm/showcases_controller.rb', line 228

def sort_images
  image_ids = params[:image_ids] || params[:digital_asset_ids]
  if image_ids.present?
    join_by_da = @showcase.showcase_digital_assets.where(digital_asset_id: image_ids).index_by(&:digital_asset_id)
    ShowcaseDigitalAsset.transaction do
      image_ids.each_with_index do |id, index|
        join_record = join_by_da[id.to_i]
        next unless join_record

        join_record.update!(position: index + 1)
      end
    end
    render json: { status: 'success' }
  else
    render json: { status: 'error', message: 'No images provided' }
  end
end

DELETE /showcases/:id/unlink_image



260
261
262
263
264
265
266
267
268
269
# File 'app/controllers/crm/showcases_controller.rb', line 260

def unlink_image
  da_id = params[:digital_asset_id].to_i
  join = @showcase.showcase_digital_assets.find_by(digital_asset_id: da_id)
  if join
    join.destroy!
    redirect_to edit_images_showcase_path(@showcase), notice: 'Image removed from showcase.'
  else
    redirect_to edit_images_showcase_path(@showcase), alert: 'Image link not found.'
  end
end

#updatevoid

This method returns an undefined value.

Updates the showcase loaded by #set_showcase.



193
194
195
196
197
198
199
# File 'app/controllers/crm/showcases_controller.rb', line 193

def update
  if @showcase.update(showcase_params)
    redirect_to showcase_path(@showcase), notice: 'Showcase was successfully updated.'
  else
    render :edit, status: :unprocessable_content
  end
end