workarea-save_for_later 1.0.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.eslintrc.json +35 -0
- data/.github/workflows/ci.yml +60 -0
- data/.gitignore +1 -2
- data/.rubocop.yml +3 -0
- data/.stylelintrc.json +8 -0
- data/CHANGELOG.md +23 -4
- data/Gemfile +2 -9
- data/README.md +47 -1
- data/app/assets/javscripts/workarea/storefront/save_for_later/modules/saved_list_analytics.js +31 -0
- data/app/controllers/workarea/admin/reports_controller.decorator +10 -0
- data/app/controllers/workarea/storefront/analytics_controller.decorator +17 -0
- data/app/controllers/workarea/storefront/current_saved_list.rb +1 -1
- data/app/helpers/workarea/storefront/saved_lists_helper.rb +11 -3
- data/app/models/workarea/insights/most_saved_products.rb +32 -0
- data/app/models/workarea/metrics/product_by_day.decorator +10 -0
- data/app/models/workarea/saved_list.rb +12 -2
- data/app/queries/workarea/order_metrics.decorator +26 -0
- data/app/queries/workarea/reports/save_for_later_products.rb +51 -0
- data/app/view_models/workarea/admin/dashboards/reports_view_model.decorator +11 -0
- data/app/view_models/workarea/admin/reports/save_for_later_products_view_model.rb +20 -0
- data/app/view_models/workarea/storefront/saved_list_view_model.rb +0 -1
- data/app/views/workarea/admin/dashboards/_save_for_later_products_card.html.haml +17 -0
- data/app/views/workarea/admin/insights/_most_saved_products.html.haml +22 -0
- data/app/views/workarea/admin/reports/save_for_later_products.html.haml +38 -0
- data/app/views/workarea/storefront/carts/_save_for_later_button.html.haml +1 -1
- data/app/views/workarea/storefront/carts/_saved_for_later.html.haml +4 -1
- data/app/workers/workarea/save_order_metrics.decorator +21 -0
- data/config/initializers/appends.rb +10 -0
- data/config/initializers/config.rb +5 -1
- data/config/locales/en.yml +21 -0
- data/config/routes.rb +11 -0
- data/lib/workarea/save_for_later/version.rb +1 -1
- data/package.json +9 -0
- data/test/integration/workarea/storefront/current_saved_list_integration_test.rb +1 -1
- data/test/models/workarea/insights/most_saved_products_test.rb +63 -0
- data/test/queries/workarea/reports/save_for_later_products_test.rb +122 -0
- data/test/queries/workarea/save_for_later_order_metrics_test.rb +48 -0
- data/test/system/workarea/admin/save_for_later_products_report_system_test.rb +44 -0
- data/test/system/workarea/storefront/save_for_later_system_test.rb +1 -1
- data/test/system/workarea/storefront/saved_list_analytics_system_test.rb +76 -0
- data/workarea-save_for_later.gemspec +1 -3
- data/yarn.lock +3265 -0
- metadata +33 -3
@@ -0,0 +1,11 @@
|
|
1
|
+
module Workarea
|
2
|
+
decorate Admin::Dashboards::ReportsViewModel, with: :save_for_later do
|
3
|
+
def save_for_later_products
|
4
|
+
@save_for_later_products ||=
|
5
|
+
Admin::Reports::SaveForLaterProductsViewModel.wrap(
|
6
|
+
Workarea::Reports::SaveForLaterProducts.new(options),
|
7
|
+
options
|
8
|
+
)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Workarea
|
2
|
+
module Admin
|
3
|
+
module Reports
|
4
|
+
class SaveForLaterProductsViewModel < ApplicationViewModel
|
5
|
+
def results
|
6
|
+
@results ||= model.results.map do |result|
|
7
|
+
product = products.detect { |p| p.id == result['_id'] }
|
8
|
+
OpenStruct.new({ product: product }.merge(result))
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def products
|
13
|
+
@products ||= Catalog::Product.any_in(
|
14
|
+
id: model.results.map { |r| r['_id'] }
|
15
|
+
).to_a
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
.grid__cell
|
2
|
+
.card{ class: card_classes(:save_for_later_products_report, local_assigns[:active]) }
|
3
|
+
= link_to save_for_later_products_report_path, class: 'card__header' do
|
4
|
+
%span.card__header-text= t('workarea.admin.reports.save_for_later_products.title')
|
5
|
+
= inline_svg 'workarea/admin/icons/insights.svg', class: 'card__icon'
|
6
|
+
|
7
|
+
.card__body
|
8
|
+
.card__centered-content
|
9
|
+
%table
|
10
|
+
%tbody
|
11
|
+
- dashboard.save_for_later_products.results.take(4).each do |result|
|
12
|
+
%tr
|
13
|
+
%td= result.product.present? ? result.product.name : result._id
|
14
|
+
%td.align-right= number_to_currency(result.revenue)
|
15
|
+
|
16
|
+
= link_to save_for_later_products_report_path, class: 'card__button' do
|
17
|
+
%span.button.button--small= t('workarea.admin.dashboards.reports.view_full_report')
|
@@ -0,0 +1,22 @@
|
|
1
|
+
.insight
|
2
|
+
.insight__date
|
3
|
+
%span.insight__period= insight.reporting_on.strftime('%B %Y')
|
4
|
+
.insight__heading= t('workarea.admin.insights.most_saved_products.title')
|
5
|
+
.insight__body
|
6
|
+
%p.insight__note= t('workarea.admin.insights.most_saved_products.info')
|
7
|
+
.grid.grid--large.grid--center
|
8
|
+
- insight.results.each do |result|
|
9
|
+
.grid__cell.grid__cell--50.grid__cell--25-at-wide.align-center
|
10
|
+
.insight__product
|
11
|
+
- if result.product.blank?
|
12
|
+
= image_tag(product_image_url(Workarea::Catalog::ProductPlaceholderImage.cached, :small), alt: result.product_id, class: 'insight__product-image')
|
13
|
+
- else
|
14
|
+
= link_to catalog_product_path(result.product) do
|
15
|
+
= image_tag(product_image_url(result.product.primary_image, :small), alt: result.product.name, class: 'insight__product-image')
|
16
|
+
.insight__product-name
|
17
|
+
- if result.product.blank?
|
18
|
+
= result.product_id
|
19
|
+
- else
|
20
|
+
= link_to result.product.name, catalog_product_path(result.product)
|
21
|
+
.insight__product-info
|
22
|
+
%strong= t('workarea.admin.insights.most_saved_products.times', count: result.adds)
|
@@ -0,0 +1,38 @@
|
|
1
|
+
- @page_title = t('workarea.admin.reports.save_for_later_products.title')
|
2
|
+
|
3
|
+
.view
|
4
|
+
.view__header
|
5
|
+
.view__heading
|
6
|
+
= link_to "↑ #{t('workarea.admin.reports.all_reports')}", reports_dashboards_path
|
7
|
+
%h1.heading.heading--no-margin= t('workarea.admin.reports.save_for_later_products.title')
|
8
|
+
%p= t('workarea.admin.reports.reference_link_html', path: reference_report_path)
|
9
|
+
|
10
|
+
.view__container.view__container--narrow
|
11
|
+
.browsing-controls.browsing-controls--with-divider.browsing-controls--center.browsing-controls--filters-displayed
|
12
|
+
= form_tag save_for_later_products_report_path, method: 'get', class: 'browsing-controls__form' do
|
13
|
+
= render 'workarea/admin/shared/date_selector', starts_at: @report.starts_at, ends_at: @report.ends_at
|
14
|
+
|
15
|
+
.browsing-controls__count
|
16
|
+
= render_reports_results_message(@report)
|
17
|
+
= render 'workarea/admin/reports/export', report: @report
|
18
|
+
|
19
|
+
%table
|
20
|
+
%thead
|
21
|
+
%tr
|
22
|
+
%th= t('workarea.admin.fields.product')
|
23
|
+
%th.align-center= link_to_reports_sorting t('workarea.admin.fields.adds'), report: @report, sort_by: 'adds'
|
24
|
+
%th.align-center= link_to_reports_sorting t('workarea.admin.fields.deletes'), report: @report, sort_by: 'deletes'
|
25
|
+
%th.align-center= link_to_reports_sorting t('workarea.admin.fields.units_sold'), report: @report, sort_by: 'units_sold'
|
26
|
+
%th.align-right= link_to_reports_sorting t('workarea.admin.fields.revenue'), report: @report, sort_by: 'revenue'
|
27
|
+
%tbody
|
28
|
+
- @report.results.each do |result|
|
29
|
+
%tr
|
30
|
+
%td
|
31
|
+
- if result.product.present?
|
32
|
+
= link_to result.product.name, catalog_product_path(result.product)
|
33
|
+
- else
|
34
|
+
= result._id
|
35
|
+
%td.align-center= number_with_delimiter result.adds
|
36
|
+
%td.align-center= number_with_delimiter result.deletes
|
37
|
+
%td.align-center= number_with_delimiter result.units_sold
|
38
|
+
%td.align-right= number_to_currency(result.revenue)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
.grid__cell
|
2
|
-
= form_tag saved_list_items_path, method: 'post', class: 'action-group__item', data: { analytics:
|
2
|
+
= form_tag saved_list_items_path, method: 'post', class: 'action-group__item', data: { analytics: add_to_saved_list_analytics_data(item.product).to_json } do
|
3
3
|
= hidden_field_tag :product_id, item.product_id, id: nil
|
4
4
|
= hidden_field_tag :sku, item.sku, id: nil
|
5
5
|
= hidden_field_tag :quantity, item.quantity, id: nil
|
@@ -24,6 +24,8 @@
|
|
24
24
|
= hidden_field_tag :sku, item.sku, id: nil
|
25
25
|
= hidden_field_tag :quantity, item.quantity, id: nil
|
26
26
|
= hidden_field_tag :saved_list_item_id, item.id, id: nil
|
27
|
+
= hidden_field_tag :via, current_saved_list.to_gid_param, id: nil
|
28
|
+
|
27
29
|
- if item.customizations.present?
|
28
30
|
- item.customizations.each do |key, value|
|
29
31
|
= hidden_field_tag key, value
|
@@ -32,7 +34,8 @@
|
|
32
34
|
- else
|
33
35
|
%p.unavailable= t('workarea.storefront.products.unavailable')
|
34
36
|
.grid__cell
|
35
|
-
=
|
37
|
+
= form_tag saved_list_item_path(item), method: :delete, data: { deletion_form: { message: t('workarea.storefront.saved_lists.confirm_remove') }, analytics: remove_from_saved_list_analytics_data(item.product).to_json } do
|
38
|
+
= button_tag t('workarea.storefront.saved_lists.remove'), class: 'text-button'
|
36
39
|
|
37
40
|
.product-list__item-cell
|
38
41
|
%table.table
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Workarea
|
2
|
+
decorate SaveOrderMetrics, with: :save_for_later do
|
3
|
+
class_methods do
|
4
|
+
def save_catalog(metrics)
|
5
|
+
super
|
6
|
+
|
7
|
+
save_saved_lists(metrics)
|
8
|
+
end
|
9
|
+
|
10
|
+
def save_saved_lists(metrics)
|
11
|
+
metrics.products_from_saved_lists.each do |product_id, data|
|
12
|
+
Metrics::ProductByDay.inc(
|
13
|
+
key: { product_id: product_id },
|
14
|
+
at: metrics.placed_at,
|
15
|
+
**data
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -12,3 +12,13 @@ Workarea.append_stylesheets(
|
|
12
12
|
'storefront.components',
|
13
13
|
'workarea/storefront/save_for_later/components/product_prices'
|
14
14
|
)
|
15
|
+
|
16
|
+
Workarea.append_javascripts(
|
17
|
+
'storefront.modules',
|
18
|
+
'workarea/storefront/save_for_later/modules/saved_list_analytics'
|
19
|
+
)
|
20
|
+
|
21
|
+
Workarea.append_partials(
|
22
|
+
'admin.reports_dashboard',
|
23
|
+
'workarea/admin/dashboards/save_for_later_products_card'
|
24
|
+
)
|
@@ -1,3 +1,7 @@
|
|
1
1
|
Workarea.configure do |config|
|
2
|
-
#
|
2
|
+
# How long to wait before clearing out old, anonymous saved list
|
3
|
+
# records. The default is 3 months, and this configures MongoDB's TTL
|
4
|
+
# for the collection. Indexes will need to be rebuilt if this option
|
5
|
+
# is changed.
|
6
|
+
config.saved_lists_expiration = 3.months
|
3
7
|
end
|
data/config/locales/en.yml
CHANGED
@@ -1,5 +1,26 @@
|
|
1
1
|
en:
|
2
2
|
workarea:
|
3
|
+
admin:
|
4
|
+
fields:
|
5
|
+
adds: Adds
|
6
|
+
deletes: Deletes
|
7
|
+
insights:
|
8
|
+
most_saved_products:
|
9
|
+
title: Most Saved Products
|
10
|
+
info: Your products most frequently saved for later from the cart.
|
11
|
+
times:
|
12
|
+
one: '%{count} time'
|
13
|
+
other: '%{count} times'
|
14
|
+
reports:
|
15
|
+
reference:
|
16
|
+
adds:
|
17
|
+
name: Adds
|
18
|
+
description: The total number of items added to a list.
|
19
|
+
deletes:
|
20
|
+
name: Deletes
|
21
|
+
description: The total number of items deleted from a list
|
22
|
+
save_for_later_products:
|
23
|
+
title: Save For Later Products
|
3
24
|
storefront:
|
4
25
|
carts:
|
5
26
|
save_for_later: Save for Later
|
data/config/routes.rb
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
Workarea::Storefront::Engine.routes.draw do
|
2
2
|
scope '(:locale)', constraints: Workarea::I18n.routes_constraint do
|
3
3
|
resources :saved_list_items, only: [:create, :destroy]
|
4
|
+
|
5
|
+
post 'analytics/saved_list_add/:product_id', to: 'analytics#saved_list_add', as: :analytics_saved_list_add
|
6
|
+
post 'analytics/saved_list_remove/:product_id', to: 'analytics#saved_list_remove', as: :analytics_saved_list_remove
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
Workarea::Admin::Engine.routes.draw do
|
11
|
+
scope '(:locale)', constraints: Workarea::I18n.routes_constraint do
|
12
|
+
resource :report, only: [] do
|
13
|
+
get :save_for_later_products
|
14
|
+
end
|
4
15
|
end
|
5
16
|
end
|
data/package.json
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Workarea
|
4
|
+
module Insights
|
5
|
+
class MostSavedProductsTest < TestCase
|
6
|
+
setup :add_data, :time_travel
|
7
|
+
|
8
|
+
def add_data
|
9
|
+
Metrics::ProductByDay.inc(
|
10
|
+
key: { product_id: 'foo' },
|
11
|
+
at: Time.zone.local(2018, 10, 27),
|
12
|
+
saved_list_adds: 1
|
13
|
+
)
|
14
|
+
|
15
|
+
Metrics::ProductByDay.inc(
|
16
|
+
key: { product_id: 'foo' },
|
17
|
+
at: Time.zone.local(2018, 10, 28),
|
18
|
+
saved_list_adds: 2
|
19
|
+
)
|
20
|
+
|
21
|
+
Metrics::ProductByDay.inc(
|
22
|
+
key: { product_id: 'foo' },
|
23
|
+
at: Time.zone.local(2018, 10, 29),
|
24
|
+
saved_list_adds: 3
|
25
|
+
)
|
26
|
+
|
27
|
+
Metrics::ProductByDay.inc(
|
28
|
+
key: { product_id: 'bar' },
|
29
|
+
at: Time.zone.local(2018, 10, 27),
|
30
|
+
saved_list_adds: 3
|
31
|
+
)
|
32
|
+
|
33
|
+
Metrics::ProductByDay.inc(
|
34
|
+
key: { product_id: 'bar' },
|
35
|
+
at: Time.zone.local(2018, 10, 28),
|
36
|
+
saved_list_adds: 4
|
37
|
+
)
|
38
|
+
|
39
|
+
Metrics::ProductByDay.inc(
|
40
|
+
key: { product_id: 'bar' },
|
41
|
+
at: Time.zone.local(2018, 10, 29),
|
42
|
+
saved_list_adds: 5
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def time_travel
|
47
|
+
travel_to Time.zone.local(2018, 11, 1)
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_generate_monthly!
|
51
|
+
MostSavedProducts.generate_monthly!
|
52
|
+
assert_equal(1, MostSavedProducts.count)
|
53
|
+
|
54
|
+
saved_products = MostSavedProducts.first
|
55
|
+
assert_equal(2, saved_products.results.size)
|
56
|
+
assert_equal('bar', saved_products.results.first['product_id'])
|
57
|
+
assert_in_delta(12, saved_products.results.first['adds'])
|
58
|
+
assert_equal('foo', saved_products.results.second['product_id'])
|
59
|
+
assert_in_delta(6, saved_products.results.second['adds'])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Workarea
|
4
|
+
module Reports
|
5
|
+
class SaveForLaterProductsTest < TestCase
|
6
|
+
setup :add_data, :time_travel
|
7
|
+
|
8
|
+
def add_data
|
9
|
+
Metrics::ProductByDay.inc(
|
10
|
+
key: { product_id: 'foo' },
|
11
|
+
at: Time.zone.local(2018, 10, 27),
|
12
|
+
saved_list_units_sold: 2,
|
13
|
+
saved_list_revenue: 10.to_m,
|
14
|
+
saved_list_adds: 2,
|
15
|
+
saved_list_deletes: 1
|
16
|
+
)
|
17
|
+
|
18
|
+
Metrics::ProductByDay.inc(
|
19
|
+
key: { product_id: 'foo' },
|
20
|
+
at: Time.zone.local(2018, 10, 28),
|
21
|
+
saved_list_units_sold: 4,
|
22
|
+
saved_list_revenue: 15.to_m,
|
23
|
+
saved_list_adds: 1,
|
24
|
+
saved_list_deletes: 0
|
25
|
+
)
|
26
|
+
|
27
|
+
Metrics::ProductByDay.inc(
|
28
|
+
key: { product_id: 'foo' },
|
29
|
+
at: Time.zone.local(2018, 10, 29),
|
30
|
+
saved_list_units_sold: 6,
|
31
|
+
saved_list_revenue: 27.to_m,
|
32
|
+
saved_list_adds: 1,
|
33
|
+
saved_list_deletes: 1
|
34
|
+
)
|
35
|
+
|
36
|
+
Metrics::ProductByDay.inc(
|
37
|
+
key: { product_id: 'bar' },
|
38
|
+
at: Time.zone.local(2018, 10, 27),
|
39
|
+
saved_list_units_sold: 3,
|
40
|
+
saved_list_revenue: 11.to_m,
|
41
|
+
saved_list_adds: 2,
|
42
|
+
saved_list_deletes: 2
|
43
|
+
)
|
44
|
+
|
45
|
+
Metrics::ProductByDay.inc(
|
46
|
+
key: { product_id: 'bar' },
|
47
|
+
at: Time.zone.local(2018, 10, 28),
|
48
|
+
saved_list_units_sold: 5,
|
49
|
+
saved_list_revenue: 15.to_m,
|
50
|
+
saved_list_adds: 1,
|
51
|
+
saved_list_deletes: 1
|
52
|
+
)
|
53
|
+
|
54
|
+
Metrics::ProductByDay.inc(
|
55
|
+
key: { product_id: 'bar' },
|
56
|
+
at: Time.zone.local(2018, 10, 29),
|
57
|
+
saved_list_units_sold: 7,
|
58
|
+
saved_list_revenue: 27.to_m,
|
59
|
+
saved_list_adds: 0,
|
60
|
+
saved_list_deletes: 1
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
def time_travel
|
65
|
+
travel_to Time.zone.local(2018, 10, 30)
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_grouping_and_summing
|
69
|
+
report = SaveForLaterProducts.new
|
70
|
+
assert_equal(2, report.results.length)
|
71
|
+
|
72
|
+
foo = report.results.detect { |r| r['_id'] == 'foo' }
|
73
|
+
assert_equal(12, foo['units_sold'])
|
74
|
+
assert_equal(52, foo['revenue'])
|
75
|
+
assert_equal(4, foo['adds'])
|
76
|
+
assert_equal(2, foo['deletes'])
|
77
|
+
|
78
|
+
bar = report.results.detect { |r| r['_id'] == 'bar' }
|
79
|
+
assert_equal(15, bar['units_sold'])
|
80
|
+
assert_equal(53, bar['revenue'])
|
81
|
+
assert_equal(3, bar['adds'])
|
82
|
+
assert_equal(4, bar['deletes'])
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_date_ranges
|
86
|
+
report = SaveForLaterProducts.new
|
87
|
+
foo = report.results.detect { |r| r['_id'] == 'foo' }
|
88
|
+
assert_equal(4, foo['adds'])
|
89
|
+
|
90
|
+
report = SaveForLaterProducts.new(starts_at: '2018-10-28', ends_at: '2018-10-28')
|
91
|
+
foo = report.results.detect { |r| r['_id'] == 'foo' }
|
92
|
+
assert_equal(1, foo['adds'])
|
93
|
+
|
94
|
+
report = SaveForLaterProducts.new(starts_at: '2018-10-28', ends_at: '2018-10-29')
|
95
|
+
foo = report.results.detect { |r| r['_id'] == 'foo' }
|
96
|
+
assert_equal(2, foo['adds'])
|
97
|
+
|
98
|
+
report = SaveForLaterProducts.new(starts_at: '2018-10-28')
|
99
|
+
foo = report.results.detect { |r| r['_id'] == 'foo' }
|
100
|
+
assert_equal(2, foo['adds'])
|
101
|
+
|
102
|
+
report = SaveForLaterProducts.new(ends_at: '2018-10-28')
|
103
|
+
foo = report.results.detect { |r| r['_id'] == 'foo' }
|
104
|
+
assert_equal(3, foo['adds'])
|
105
|
+
end
|
106
|
+
|
107
|
+
def test_sorting
|
108
|
+
report = SaveForLaterProducts.new(sort_by: 'adds', sort_direction: 'asc')
|
109
|
+
assert_equal('bar', report.results.first['_id'])
|
110
|
+
|
111
|
+
report = SaveForLaterProducts.new(sort_by: 'adds', sort_direction: 'desc')
|
112
|
+
assert_equal('foo', report.results.first['_id'])
|
113
|
+
|
114
|
+
report = SaveForLaterProducts.new(sort_by: 'units_sold', sort_direction: 'asc')
|
115
|
+
assert_equal('foo', report.results.first['_id'])
|
116
|
+
|
117
|
+
report = SaveForLaterProducts.new(sort_by: 'units_sold', sort_direction: 'desc')
|
118
|
+
assert_equal('bar', report.results.first['_id'])
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|