solidus_admin_insights 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +54 -0
  3. data/Gemfile +2 -0
  4. data/LICENSE +26 -0
  5. data/README.md +71 -0
  6. data/Rakefile +21 -0
  7. data/app/assets/javascripts/spree/backend/jquery.tablesorter.min.js +4 -0
  8. data/app/assets/javascripts/spree/backend/solidus_admin_insights.js +2 -0
  9. data/app/assets/javascripts/spree/backend/solidus_admin_insights/paginator.js +128 -0
  10. data/app/assets/javascripts/spree/backend/solidus_admin_insights/report_loader.js +265 -0
  11. data/app/assets/javascripts/spree/backend/solidus_admin_insights/searcher.js +72 -0
  12. data/app/assets/javascripts/spree/backend/solidus_admin_insights/table_sorter.js +47 -0
  13. data/app/assets/javascripts/spree/backend/tmpl.js +87 -0
  14. data/app/assets/javascripts/spree/frontend/solidus_admin_insights.js +2 -0
  15. data/app/assets/stylesheets/spree/backend/override_pdf.css +71 -0
  16. data/app/assets/stylesheets/spree/backend/solidus_admin_insights.css +132 -0
  17. data/app/assets/stylesheets/spree/frontend/solidus_admin_insights.css +4 -0
  18. data/app/controllers/spree/admin/insights_controller.rb +103 -0
  19. data/app/helpers/spree/admin/base_helper_decorator.rb +17 -0
  20. data/app/models/spree/app_configuration_decorator.rb +3 -0
  21. data/app/models/spree/product_decorator.rb +3 -0
  22. data/app/models/spree/promotion_action_decorator.rb +3 -0
  23. data/app/models/spree/return_authorization_decorator.rb +4 -0
  24. data/app/permissions/spree/permission_sets/insight_display.rb +9 -0
  25. data/app/reports/spree/best_selling_products_report.rb +42 -0
  26. data/app/reports/spree/cart_additions_report.rb +36 -0
  27. data/app/reports/spree/cart_removals_report.rb +36 -0
  28. data/app/reports/spree/cart_updations_report.rb +40 -0
  29. data/app/reports/spree/payment_method_transactions_conversion_rate_report.rb +74 -0
  30. data/app/reports/spree/payment_method_transactions_conversion_rate_report/payment_method_state_distribution_chart.rb +39 -0
  31. data/app/reports/spree/payment_method_transactions_report.rb +60 -0
  32. data/app/reports/spree/payment_method_transactions_report/payment_method_revenue_distribution_chart.rb +36 -0
  33. data/app/reports/spree/product_views_report.rb +46 -0
  34. data/app/reports/spree/product_views_to_cart_additions_report.rb +53 -0
  35. data/app/reports/spree/product_views_to_purchases_report.rb +54 -0
  36. data/app/reports/spree/promotional_cost_report.rb +84 -0
  37. data/app/reports/spree/promotional_cost_report/promotional_cost_chart.rb +37 -0
  38. data/app/reports/spree/promotional_cost_report/usage_count_chart.rb +41 -0
  39. data/app/reports/spree/report.rb +131 -0
  40. data/app/reports/spree/report/chart.rb +11 -0
  41. data/app/reports/spree/report/configuration.rb +40 -0
  42. data/app/reports/spree/report/date_slicer.rb +61 -0
  43. data/app/reports/spree/report/observation.rb +49 -0
  44. data/app/reports/spree/report/query_fragments.rb +45 -0
  45. data/app/reports/spree/report/query_time_scale.rb +19 -0
  46. data/app/reports/spree/report/result.rb +100 -0
  47. data/app/reports/spree/report/timed_observation.rb +47 -0
  48. data/app/reports/spree/report/timed_result.rb +48 -0
  49. data/app/reports/spree/returned_products_report.rb +37 -0
  50. data/app/reports/spree/sales_performance_report.rb +107 -0
  51. data/app/reports/spree/sales_performance_report/profit_loss_chart.rb +37 -0
  52. data/app/reports/spree/sales_performance_report/profit_loss_percent_chart.rb +36 -0
  53. data/app/reports/spree/sales_performance_report/sale_cost_price_chart.rb +48 -0
  54. data/app/reports/spree/sales_tax_report.rb +64 -0
  55. data/app/reports/spree/sales_tax_report/monthly_sales_tax_comparison_chart.rb +39 -0
  56. data/app/reports/spree/shipping_cost_report.rb +89 -0
  57. data/app/reports/spree/shipping_cost_report/shipping_cost_distribution_chart.rb +38 -0
  58. data/app/reports/spree/trending_search_report.rb +50 -0
  59. data/app/reports/spree/trending_search_report/frequency_distribution_pie_chart.rb +41 -0
  60. data/app/reports/spree/unique_purchases_report.rb +39 -0
  61. data/app/reports/spree/user_pool_report.rb +66 -0
  62. data/app/reports/spree/user_pool_report/distribution_column_chart.rb +65 -0
  63. data/app/reports/spree/users_not_converted_report.rb +48 -0
  64. data/app/reports/spree/users_who_recently_purchased_report.rb +69 -0
  65. data/app/services/spree/report_generation_service.rb +27 -0
  66. data/app/views/spree/admin/insights/_chart.html.erb +4 -0
  67. data/app/views/spree/admin/insights/download.pdf.erb +27 -0
  68. data/app/views/spree/admin/insights/index.html.erb +82 -0
  69. data/app/views/spree/admin/insights/search/_product_views_search.html.erb +13 -0
  70. data/app/views/spree/admin/insights/search/_search_form.html.erb +39 -0
  71. data/app/views/spree/admin/insights/search/_trending_searches_search.html.erb +13 -0
  72. data/app/views/spree/admin/insights/search/_users_not_converted_search.html.erb +13 -0
  73. data/app/views/spree/admin/insights/search/_users_who_have_not_purchased_recently_search.html.erb +13 -0
  74. data/app/views/spree/admin/insights/search/_users_who_recently_purchased_search.html.erb +13 -0
  75. data/app/views/spree/admin/shared/_insights_side_menu.html.erb +5 -0
  76. data/app/views/spree/admin/shared/sub_menu/_insight.html.erb +7 -0
  77. data/app/views/spree/admin/templates/insights/_paginator.template +11 -0
  78. data/app/views/spree/admin/templates/insights/_search.template +76 -0
  79. data/app/views/spree/admin/templates/insights/_show.template +49 -0
  80. data/app/views/spree/layouts/pdf.html.erb +9 -0
  81. data/bin/rails +7 -0
  82. data/config/initializers/add_to_sidebar.rb +14 -0
  83. data/config/initializers/assets.rb +1 -0
  84. data/config/initializers/mime_types.rb +2 -0
  85. data/config/initializers/wicked_pdf.rb +21 -0
  86. data/config/locales/en.yml +167 -0
  87. data/config/routes.rb +6 -0
  88. data/lib/generators/solidus_admin_insights/install/install_generator.rb +36 -0
  89. data/lib/generators/solidus_admin_insights/install/solidus_admin_insights.rb +22 -0
  90. data/lib/solidus_admin_insights.rb +14 -0
  91. data/lib/solidus_admin_insights/engine.rb +27 -0
  92. data/lib/solidus_admin_insights/factories.rb +6 -0
  93. data/solidus_admin_insights.gemspec +42 -0
  94. data/spec/spec_helper.rb +93 -0
  95. metadata +419 -0
@@ -0,0 +1,53 @@
1
+ module Spree
2
+ class ProductViewsToCartAdditionsReport < Spree::Report
3
+ DEFAULT_SORTABLE_ATTRIBUTE = :product_name
4
+ HEADERS = { product_name: :string, views: :integer, cart_additions: :integer, cart_to_view_ratio: :string }
5
+ SEARCH_ATTRIBUTES = { start_date: :product_view_from, end_date: :product_view_till }
6
+ SORTABLE_ATTRIBUTES = [:product_name, :views, :cart_additions]
7
+
8
+ deeplink product_name: { template: %Q{<a href="/admin/products/{%# o.product_slug %}" target="_blank">{%# o.product_name %}</a>} }
9
+
10
+ class Result < Spree::Report::Result
11
+ class Observation < Spree::Report::Observation
12
+ observation_fields [:product_name, :product_slug, :views, :cart_additions, :cart_to_view_ratio]
13
+
14
+ def cart_to_view_ratio
15
+ (cart_additions.to_f / views.to_f).round(2)
16
+ end
17
+ end
18
+ end
19
+
20
+ def report_query
21
+ cart_additions =
22
+ Spree::CartEvent
23
+ .added
24
+ .joins(:variant)
25
+ .joins(:product)
26
+ .where(created_at: reporting_period)
27
+ .group('spree_products.name', 'spree_products.slug')
28
+ .select(
29
+ 'spree_products.name as product_name',
30
+ 'spree_products.slug as product_slug',
31
+ 'SUM(spree_cart_events.quantity) as cart_additions'
32
+ )
33
+ total_views =
34
+ Spree::Product
35
+ .joins(:page_view_events)
36
+ .group(:name)
37
+ .select(
38
+ 'spree_products.name as product_name',
39
+ 'COUNT(*) as views'
40
+ )
41
+
42
+ Spree::Report::QueryFragments
43
+ .from_join(cart_additions, total_views, "q1.product_name = q2.product_name")
44
+ .project(
45
+ 'q1.product_name',
46
+ 'q1.product_slug',
47
+ 'q2.views',
48
+ 'q1.cart_additions'
49
+ )
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,54 @@
1
+ module Spree
2
+ class ProductViewsToPurchasesReport < Spree::Report
3
+ DEFAULT_SORTABLE_ATTRIBUTE = :product_name
4
+ HEADERS = { product_name: :string, views: :integer, purchases: :integer, purchase_to_view_ratio: :integer }
5
+ SEARCH_ATTRIBUTES = { start_date: :product_view_from, end_date: :product_view_till }
6
+ SORTABLE_ATTRIBUTES = [:product_name, :views, :purchases]
7
+
8
+ class Result < Spree::Report::Result
9
+ class Observation < Spree::Report::Observation
10
+ observation_fields [:product_name, :product_slug, :views, :purchases, :purchase_to_view_ratio]
11
+
12
+ def purchase_to_view_ratio # This is inconsistent across postgres and mysql
13
+ (purchases.to_f / views.to_f).round(2)
14
+ end
15
+ end
16
+ end
17
+
18
+ deeplink product_name: { template: %Q{<a href="/admin/products/{%# o.product_slug %}" target="_blank">{%# o.product_name %}</a>} }
19
+
20
+ def report_query
21
+ page_events_ar = Arel::Table.new(:spree_page_events)
22
+ purchase_line_items_ar = Arel::Table.new(:purchase_line_items)
23
+
24
+ Spree::Report::QueryFragments.from_subquery(purchase_line_items, as: :purchase_line_items)
25
+ .join(page_events_ar)
26
+ .on(page_events_ar[:target_id].eq(purchase_line_items_ar[:product_id]))
27
+ .where(page_events_ar[:target_type].eq(Arel::Nodes::Quoted.new('Spree::Product')))
28
+ .where(page_events_ar[:activity].eq(Arel::Nodes::Quoted.new('view')))
29
+ .group(purchase_line_items_ar[:product_id], purchase_line_items_ar[:product_name],
30
+ purchase_line_items_ar[:product_slug], purchase_line_items_ar[:purchases])
31
+ .project(
32
+ 'product_name',
33
+ 'product_slug',
34
+ 'COUNT(*) as views',
35
+ 'purchases'
36
+ )
37
+ end
38
+
39
+ private def purchase_line_items
40
+ Spree::LineItem
41
+ .joins(:order)
42
+ .joins(:variant)
43
+ .joins(:product)
44
+ .where(spree_orders: { state: 'complete', created_at: reporting_period })
45
+ .group('spree_products.id', 'spree_products.name')
46
+ .select(
47
+ 'SUM(quantity) as purchases',
48
+ 'spree_products.name as product_name',
49
+ 'spree_products.slug as product_slug',
50
+ 'spree_products.id as product_id'
51
+ )
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,84 @@
1
+ module Spree
2
+ class PromotionalCostReport < Spree::Report
3
+ DEFAULT_SORTABLE_ATTRIBUTE = :promotion_name
4
+ HEADERS = { promotion_name: :string, usage_count: :integer, promotion_discount: :integer, promotion_code: :string, promotion_start_date: :date, promotion_end_date: :date }
5
+ SEARCH_ATTRIBUTES = { start_date: :promotion_created_from, end_date: :promotion_created_till }
6
+ SORTABLE_ATTRIBUTES = [:promotion_name, :usage_count, :promotion_discount, :promotion_code, :promotion_start_date, :promotion_end_date]
7
+
8
+ class Result < Spree::Report::TimedResult
9
+ charts PromotionalCostChart, UsageCountChart
10
+
11
+ def build_empty_observations
12
+ super
13
+ @_promotions = @results.collect { |result| result['promotion_name'] }.uniq
14
+ @observations = @_promotions.collect do |promotion_name|
15
+ @observations.collect do |observation|
16
+ _d_observation = observation.dup
17
+ _d_observation.promotion_name = promotion_name
18
+ _d_observation.usage_count = 0
19
+ _d_observation
20
+ end
21
+ end.flatten
22
+ end
23
+
24
+ class Observation < Spree::Report::TimedObservation
25
+ observation_fields [
26
+ :promotion_name, :usage_count,
27
+ :promotion_discount, :promotion_code,
28
+ :promotion_start_date, :promotion_end_date
29
+ ]
30
+
31
+ def promotion_start_date
32
+ @promotion_start_date.present? ? @promotion_start_date.to_date.strftime("%B %d %Y") : "-"
33
+ end
34
+
35
+ def promotion_end_date
36
+ @promotion_end_date.present? ? @promotion_end_date.to_date.strftime("%B %d %Y") : "-"
37
+ end
38
+
39
+ def promotion_discount
40
+ @promotion_discount.to_f.abs
41
+ end
42
+
43
+ def describes?(result, time_scale)
44
+ result['promotion_name'] == promotion_name && super
45
+ end
46
+ end
47
+ end
48
+
49
+ def report_query
50
+ Spree::Report::QueryFragments
51
+ .from_subquery(eligible_promotions)
52
+ .group(*time_scale_columns, :promotion_id, :promotion_name,
53
+ :promotion_code, :promotion_start_date, :promotion_end_date)
54
+ .order(*time_scale_columns_to_s)
55
+ .project(
56
+ *time_scale_columns,
57
+ 'promotion_name',
58
+ 'promotion_code',
59
+ 'promotion_start_date',
60
+ 'promotion_end_date',
61
+ 'SUM(promotion_discount) as promotion_discount',
62
+ 'COUNT(promotion_id) as usage_count',
63
+ 'promotion_id'
64
+ )
65
+ end
66
+
67
+ private def eligible_promotions
68
+ Spree::PromotionAction
69
+ .joins(:promotion)
70
+ .joins(:adjustment)
71
+ .where(spree_adjustments: { created_at: reporting_period })
72
+ .select(
73
+ 'spree_promotions.starts_at as promotion_start_date',
74
+ 'spree_promotions.expires_at as promotion_end_date',
75
+ 'spree_adjustments.amount as promotion_discount',
76
+ 'spree_promotions.id as promotion_id',
77
+ 'spree_promotions.name as promotion_name',
78
+ 'spree_promotions.code as promotion_code',
79
+ *time_scale_selects('spree_adjustments')
80
+ )
81
+ end
82
+
83
+ end
84
+ end
@@ -0,0 +1,37 @@
1
+ class Spree::PromotionalCostReport::PromotionalCostChart
2
+ attr_accessor :time, :series
3
+
4
+ def initialize(result)
5
+ @grouped_by_promotion = result.observations.group_by(&:promotion_name)
6
+ @time_dimension = result.time_dimension
7
+ self.time = []
8
+ self.time = @grouped_by_promotion.values.first.collect { |observation_value| observation_value.send(@time_dimension) } if @grouped_by_promotion.first.present?
9
+ self.series = @grouped_by_promotion.collect { |promotion, values| { type: 'column', name: promotion, data: values.collect(&:promotion_discount) } }
10
+ end
11
+
12
+ def to_h
13
+ {
14
+ id: 'promotional-cost',
15
+ json: {
16
+ chart: { type: 'column' },
17
+ title: {
18
+ useHTML: true,
19
+ text: "<span class='chart-title'>Promotional Cost</span><span class='fa fa-question-circle' data-toggle='tooltip' title=' Compare the costing for various promotions'></span>"
20
+ },
21
+ xAxis: { categories: time },
22
+ yAxis: {
23
+ title: { text: 'Value($)' }
24
+ },
25
+ tooltip: { valuePrefix: '$' },
26
+ legend: {
27
+ layout: 'vertical',
28
+ align: 'right',
29
+ verticalAlign: 'middle',
30
+ borderWidth: 0
31
+ },
32
+ series: series
33
+ }
34
+ }
35
+ end
36
+
37
+ end
@@ -0,0 +1,41 @@
1
+ class Spree::PromotionalCostReport::UsageCountChart
2
+
3
+ attr_accessor :time, :series
4
+
5
+ def initialize(result)
6
+ @grouped_by_promotion = result.observations.group_by(&:promotion_name)
7
+ @time_dimension = result.time_dimension
8
+ self.time = []
9
+ if @grouped_by_promotion.values.first.present?
10
+ self.time = @grouped_by_promotion.values.first.collect { |observation_value| observation_value.send(@time_dimension) }
11
+ end
12
+ self.series = @grouped_by_promotion.collect { |promotion, values| { type: 'column', name: promotion, data: values.collect(&:usage_count) } }
13
+ end
14
+
15
+
16
+ def to_h
17
+ {
18
+ id: 'promotion-usage-count',
19
+ json: {
20
+ chart: { type: 'spline' },
21
+ title: {
22
+ useHTML: true,
23
+ text: "<span class='chart-title'>Promotion Usage Count</span><span class='fa fa-question-circle' data-toggle='tooltip' title='Compare the usage of individual promotions'></span>"
24
+ },
25
+ xAxis: { categories: time },
26
+ yAxis: {
27
+ title: { text: 'Count' }
28
+ },
29
+ tooltip: { valuePrefix: '#' },
30
+ legend: {
31
+ layout: 'vertical',
32
+ align: 'right',
33
+ verticalAlign: 'middle',
34
+ borderWidth: 0
35
+ },
36
+ series: series
37
+ }
38
+ }
39
+ end
40
+
41
+ end
@@ -0,0 +1,131 @@
1
+ module Spree
2
+ class Report
3
+
4
+ attr_accessor :sortable_attribute, :sortable_type, :total_records,
5
+ :records_per_page, :current_page, :paginate, :search, :reporting_period
6
+ alias_method :sort_direction, :sortable_type
7
+ alias_method :paginate?, :paginate
8
+
9
+
10
+ TIME_SCALES = [:hourly, :daily, :monthly, :yearly]
11
+
12
+ def paginated?
13
+ false
14
+ end
15
+
16
+ def pagination_required?
17
+ paginated? && paginate?
18
+ end
19
+
20
+ def deeplink_properties
21
+ {
22
+ deeplinked: false
23
+ }
24
+ end
25
+
26
+ def self.deeplink(template_for_headers = {})
27
+ define_method :deeplink_properties do
28
+ { deeplinked: true }.merge(template_for_headers)
29
+ end
30
+ end
31
+
32
+ def generate(options = {})
33
+ self.class::Result.new do |report|
34
+ report.start_date = @start_date
35
+ report.end_date = @end_date
36
+ report.time_scale = @time_scale
37
+ report.report = self
38
+ end
39
+ end
40
+
41
+
42
+ def initialize(options)
43
+ self.search = options.fetch(:search, {})
44
+ self.records_per_page = options[:records_per_page]
45
+ self.current_page = options[:offset]
46
+ self.paginate = options[:paginate]
47
+ extract_reporting_period
48
+ determine_report_time_scale
49
+ if self.class::SORTABLE_ATTRIBUTES.present?
50
+ set_sortable_attributes(options, self.class::DEFAULT_SORTABLE_ATTRIBUTE)
51
+ end
52
+ end
53
+
54
+ def header_sorted?(header)
55
+ sortable_attribute.present? && sortable_attribute.eql?(header)
56
+ end
57
+
58
+ def get_results
59
+ query =
60
+ if pagination_required?
61
+ paginated_report_query
62
+ else
63
+ report_query
64
+ end
65
+
66
+ query = query.order(active_record_sort) if sortable_attribute.present?
67
+ query_sql = query.to_sql
68
+ r = ActiveRecord::Base.connection.exec_query(query_sql)
69
+ end
70
+
71
+ def set_sortable_attributes(options, default_sortable_attribute)
72
+ self.sortable_type ||= (options[:sort] && options[:sort][:type].eql?('desc')) ? :desc : :asc
73
+ self.sortable_attribute = options[:sort] ? options[:sort][:attribute].to_sym : default_sortable_attribute
74
+ end
75
+
76
+ def active_record_sort
77
+ "#{ sortable_attribute } #{ sortable_type }"
78
+ end
79
+
80
+ def total_records
81
+ ActiveRecord::Base.connection.select_value(record_count_query.to_sql)
82
+ end
83
+
84
+ def total_pages
85
+ if pagination_required?
86
+ total_pages = total_records / records_per_page
87
+ total_pages -= 1 if total_records % records_per_page == 0
88
+ total_pages
89
+ end
90
+ end
91
+
92
+ def time_scale_selects(time_scale_on = nil)
93
+ QueryTimeScale.select(@time_scale, time_scale_on)
94
+ end
95
+
96
+ def time_scale_columns
97
+ @_time_scale_columns ||= QueryTimeScale.time_scale_columns(@time_scale)
98
+ end
99
+
100
+ def time_scale_columns_to_s
101
+ @_time_scale_columns_to_s ||= time_scale_columns.collect(&:to_s)
102
+ end
103
+
104
+ def name
105
+ @_report_name ||= self.class.to_s.demodulize.underscore.gsub("_report", "")
106
+ end
107
+
108
+ private def extract_reporting_period
109
+ start_date = @search[:start_date]
110
+ @start_date = start_date.present? ? Date.parse(start_date) : Date.current.beginning_of_year
111
+ end_date = @search[:end_date]
112
+ @end_date = (end_date.present? ? Date.parse(end_date).next_day : Date.current.end_of_year)
113
+ self.reporting_period = (@start_date.beginning_of_day)..(@end_date.end_of_day)
114
+ end
115
+
116
+ private def determine_report_time_scale
117
+ @time_scale =
118
+ case (@end_date - @start_date).to_i
119
+ when 0..1
120
+ :hourly
121
+ when 1..60
122
+ :daily
123
+ when 61..600
124
+ :monthly
125
+ else
126
+ :yearly
127
+ end
128
+ end
129
+
130
+ end
131
+ end
@@ -0,0 +1,11 @@
1
+ module Spree
2
+ class Report
3
+ module Chart
4
+ extend ActiveSupport::Concern
5
+
6
+ def chart_json
7
+ self.class::Chart.new(self, time_dimension).to_h
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,40 @@
1
+ class Spree::Report::Configuration
2
+ attr_accessor :default_report_category, :default_report
3
+ attr_reader :reports
4
+
5
+ def initialize
6
+ @reports = {}
7
+ end
8
+
9
+ def register_report_category(category)
10
+ @reports[category] = []
11
+ end
12
+
13
+ def register_report(category, report_name)
14
+ @reports[category] << report_name
15
+ end
16
+
17
+ def report_exists?(category, name)
18
+ @reports.key?(category) && @reports[category].include?(name)
19
+ end
20
+
21
+ def reports_for_category(category)
22
+ if category_exists? category
23
+ @reports[category]
24
+ else
25
+ []
26
+ end
27
+ end
28
+
29
+ def default_report_category
30
+ @default_report_category || @reports.keys.first
31
+ end
32
+
33
+ def default_report
34
+ @default_report || @reports[default_report_category].first
35
+ end
36
+
37
+ def category_exists?(category)
38
+ @reports.key? category
39
+ end
40
+ end