solidus_admin_insights 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,61 @@
1
+ module Spree::Report::DateSlicer
2
+ def self.slice_into(start_date, end_date, time_scale, klass)
3
+ case time_scale
4
+ when :hourly
5
+ slice_hours_into(start_date, end_date, klass)
6
+ when :daily
7
+ slice_days_into(start_date, end_date, klass)
8
+ when :monthly
9
+ slice_months_into(start_date, end_date, klass)
10
+ when :yearly
11
+ slice_years_into(start_date, end_date, klass)
12
+ end
13
+ end
14
+
15
+ def self.slice_hours_into(start_date, end_date, klass)
16
+ current_date = start_date
17
+ slices = []
18
+ while current_date < end_date
19
+ slices << (0..23).collect do |hour|
20
+ obj = klass.new
21
+ obj.date = current_date
22
+ obj.hour = hour
23
+ obj
24
+ end
25
+ current_date = current_date.next_day
26
+ end
27
+ slices.flatten
28
+ end
29
+
30
+ def self.slice_days_into(start_date, end_date, klass)
31
+ current_date = start_date
32
+ slices = []
33
+ while current_date < end_date
34
+ obj = klass.new
35
+ obj.date = current_date
36
+ slices << obj
37
+ current_date = current_date.next_day
38
+ end
39
+ slices
40
+ end
41
+
42
+ def self.slice_months_into(start_date, end_date, klass)
43
+ current_date = start_date
44
+ slices = []
45
+ while current_date < end_date
46
+ obj = klass.new
47
+ obj.date = current_date
48
+ slices << obj
49
+ current_date = current_date.end_of_month.next_day
50
+ end
51
+ slices
52
+ end
53
+
54
+ def self.slice_years_into(start_date, end_date, klass)
55
+ (start_date.year..end_date.year).collect do |year|
56
+ obj = klass.new
57
+ obj.date = Date.new(year).end_of_year
58
+ obj
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,49 @@
1
+ class Spree::Report::Observation
2
+
3
+ def initialize
4
+ set_defaults
5
+ end
6
+
7
+ class << self
8
+ def observation_fields(records)
9
+ case records
10
+ when Hash
11
+ build_from_hash(records)
12
+ else
13
+ build_from_list(records)
14
+ end
15
+ end
16
+
17
+ def build_from_hash(records)
18
+ build_from_list(records.keys)
19
+
20
+ define_method :set_defaults do
21
+ records.keys.each do |key|
22
+ self.send("#{ key }=", records[key])
23
+ end
24
+ end
25
+ end
26
+
27
+ def build_from_list(records)
28
+ attr_accessor *records
29
+
30
+ define_method :populate do |result|
31
+ records.each do |record|
32
+ record_name = record.to_s
33
+ self.send("#{ record }=", result[record_name]) if result[record_name]
34
+ end
35
+ end
36
+
37
+ define_method :set_defaults do
38
+ end
39
+
40
+ define_method :observations_to_h do
41
+ records.inject({}) { |acc, record| acc[record] = self.send(record); acc }
42
+ end
43
+ end
44
+ end
45
+
46
+ def to_h
47
+ observations_to_h
48
+ end
49
+ end
@@ -0,0 +1,45 @@
1
+ module Spree::Report::QueryFragments
2
+ def self.from_subquery(subquery, as: 'results')
3
+ Arel::SelectManager.new(Arel.sql("(#{subquery.to_sql}) as #{ as }"))
4
+ end
5
+
6
+ def self.from_join(subquery1, subquery2, join_expr)
7
+ Arel::SelectManager.new(Arel.sql("((#{ subquery1.to_sql }) as q1 JOIN (#{ subquery2.to_sql }) as q2 ON #{ join_expr })"))
8
+ end
9
+
10
+ def self.from_union(subquery1, subquery2, as: 'results')
11
+ Arel::SelectManager.new(Arel.sql("((#{ subquery1.to_sql }) UNION (#{ subquery2.to_sql })) as #{ as }"))
12
+ end
13
+
14
+ def self.year(column, as='year')
15
+ extract_from_date(:year, column, as)
16
+ end
17
+
18
+ def self.month(column, as='month')
19
+ extract_from_date(:month, column, as)
20
+ end
21
+
22
+ def self.week(column, as='week')
23
+ extract_from_date(:week, column, as)
24
+ end
25
+
26
+ def self.day(column, as='day')
27
+ extract_from_date(:day, column, as)
28
+ end
29
+
30
+ def self.hour(column, as='hour')
31
+ extract_from_date(:hour, column, as)
32
+ end
33
+
34
+ def self.extract_from_date(part, column, as)
35
+ "EXTRACT(#{ part } from #{ column }) AS #{ as }"
36
+ end
37
+
38
+ def self.if_null(val, default_val)
39
+ Arel::Nodes::NamedFunction.new('COALESCE', [val, default_val])
40
+ end
41
+
42
+ def self.sum(node)
43
+ Arel::Nodes::NamedFunction.new('SUM', [node])
44
+ end
45
+ end
@@ -0,0 +1,19 @@
1
+ module Spree::Report::QueryTimeScale
2
+ def self.select(time_scale, time_scale_on)
3
+ db_col_name = time_scale_on.present? ? "#{ time_scale_on }.created_at" : "created_at"
4
+ time_scale_columns(time_scale).collect { |time_scale_column| ::Spree::Report::QueryFragments.public_send(time_scale_column, db_col_name) }
5
+ end
6
+
7
+ def self.time_scale_columns(time_scale)
8
+ case time_scale
9
+ when :hourly
10
+ [:day, :hour]
11
+ when :daily
12
+ [:month, :day]
13
+ when :monthly
14
+ [:year, :month]
15
+ when :yearly
16
+ [:year]
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,100 @@
1
+ module Spree
2
+ class Report
3
+ class Result
4
+ attr_accessor :start_date, :end_date, :time_scale, :report
5
+ attr_reader :observations
6
+
7
+ def initialize
8
+ yield self
9
+ build_report_observations
10
+ end
11
+
12
+ def build_report_observations
13
+ query_results
14
+ populate_observations
15
+ end
16
+
17
+ def query_results
18
+ @results = report.get_results
19
+ end
20
+
21
+ def populate_observations
22
+ @observations = @results.collect do |result|
23
+ _observation = self.class::Observation.new
24
+ _observation.populate(result)
25
+ _observation
26
+ end
27
+ end
28
+
29
+
30
+ def to_h
31
+ {
32
+ deeplink: report.deeplink_properties,
33
+ total_pages: report.total_pages,
34
+ per_page: report.records_per_page,
35
+ pagination_required: report.pagination_required?,
36
+ headers: headers,
37
+ search_attributes: search_attributes,
38
+ stats: observations.collect(&:to_h),
39
+ chart_json: chart_json
40
+ }
41
+ end
42
+
43
+ def chart_json
44
+ {
45
+ chart: false,
46
+ charts: []
47
+ }
48
+ end
49
+
50
+ def self.charts(*report_charts)
51
+ define_method :chart_json do
52
+ {
53
+ chart: true,
54
+ charts: report_charts.collect { |report_chart| report_chart.new(self).to_h }.flatten
55
+ }
56
+ end
57
+ end
58
+
59
+ def search_attributes
60
+ report.class::SEARCH_ATTRIBUTES.transform_values { |value| value.to_s.humanize }
61
+ end
62
+
63
+ def total_pages # O indexed
64
+ if report.pagination_required?
65
+ total_pages = report.total_records / report.records_per_page
66
+ if report.total_records % report.records_per_page == 0
67
+ total_pages -= 1
68
+ end
69
+ total_pages
70
+ end
71
+ end
72
+
73
+ def headers
74
+ report.class::HEADERS.keys.collect do |header|
75
+ header_description = {
76
+ name: Spree.t(header.to_sym, scope: [:insight, report.name]),
77
+ value: header,
78
+ type: report.class::HEADERS[header],
79
+ sortable: header.in?(report.class::SORTABLE_ATTRIBUTES)
80
+ }
81
+ header_description[:sorted] = report.sort_direction if report.header_sorted?(header)
82
+ header_description
83
+ end
84
+ end
85
+
86
+ def time_dimension
87
+ case time_scale
88
+ when :hourly
89
+ :hour_name
90
+ when :daily
91
+ :day_name
92
+ when :monthly
93
+ :month_name
94
+ when :yearly
95
+ :year
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,47 @@
1
+ class Spree::Report::TimedObservation < Spree::Report::Observation
2
+
3
+ extend Forwardable
4
+
5
+ attr_accessor :date, :hour, :reportable_keys
6
+
7
+ def_delegators :date, :day, :month, :year
8
+
9
+ def initialize
10
+ super
11
+ self.hour = 0
12
+ end
13
+
14
+ def describes?(result, time_scale)
15
+ case time_scale
16
+ when :hourly
17
+ result['hour'] == hour && result['day'] == day
18
+ when :daily
19
+ result['day'] == day && result['month'] == month
20
+ when :monthly
21
+ result['month'] == month && result['year'] == year
22
+ when :yearly
23
+ result['year'] == year
24
+ end
25
+ end
26
+
27
+ def month_name
28
+ Date::MONTHNAMES[month]
29
+ end
30
+
31
+ def hour_name
32
+ if hour == 23
33
+ return "23:00 - 00:00"
34
+ else
35
+ return "#{ hour }:00 - #{ hour + 1 }:00"
36
+ end
37
+ end
38
+
39
+ def day_name
40
+ "#{ day } #{ month_name }"
41
+ end
42
+
43
+ def to_h
44
+ super.merge({day_name: day_name, month_name: month_name, year: year, hour_name: hour_name})
45
+ end
46
+
47
+ end
@@ -0,0 +1,48 @@
1
+ module Spree
2
+ class Report
3
+ class TimedResult < Result
4
+
5
+ def build_report_observations
6
+ query_results
7
+ build_empty_observations
8
+ populate_observations
9
+ end
10
+
11
+ def build_empty_observations
12
+ @observations = Spree::Report::DateSlicer.slice_into(start_date, end_date, time_scale, self.class::Observation)
13
+ end
14
+
15
+ def populate_observations
16
+ observation_iter = @observations.each
17
+ current_observation = @observations.present? ? observation_iter.next : nil
18
+ @results.each do |result|
19
+ if current_observation.present?
20
+ begin
21
+ until current_observation.describes? result, time_scale
22
+ current_observation = observation_iter.next
23
+ end
24
+
25
+ current_observation.populate(result)
26
+ current_observation = observation_iter.next
27
+ rescue StopIteration
28
+ break
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def headers
35
+ [time_headers] + super
36
+ end
37
+
38
+ private def time_headers
39
+ {
40
+ name: Spree.t(time_dimension, scope: [:admin]),
41
+ value: time_dimension,
42
+ type: :string,
43
+ sortable: false
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,37 @@
1
+ module Spree
2
+ class ReturnedProductsReport < Spree::Report
3
+ DEFAULT_SORTABLE_ATTRIBUTE = :product_name
4
+ HEADERS = { sku: :string, product_name: :string, return_count: :integer }
5
+ SEARCH_ATTRIBUTES = { start_date: :product_returned_from, end_date: :product_returned_till }
6
+ SORTABLE_ATTRIBUTES = [:product_name, :sku, :return_count]
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 [:sku, :product_name, :return_count, :product_slug]
13
+
14
+ def sku
15
+ @sku.presence || @product_name
16
+ end
17
+ end
18
+ end
19
+
20
+ def report_query
21
+ Spree::ReturnAuthorization
22
+ .joins(:return_items)
23
+ .joins(:inventory_units)
24
+ .joins(:variants)
25
+ .joins(:products)
26
+ .where(spree_return_items: { created_at: reporting_period })
27
+ .group('spree_variants.id', 'spree_products.name', 'spree_products.slug', 'spree_variants.sku')
28
+ .select(
29
+ 'spree_products.name as product_name',
30
+ 'spree_products.slug as product_slug',
31
+ 'spree_variants.sku as sku',
32
+ 'COUNT(spree_variants.id) as return_count'
33
+ )
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,107 @@
1
+ module Spree
2
+ class SalesPerformanceReport < Spree::Report
3
+ HEADERS = { sale_price: :integer, cost_price: :integer, promotion_discount: :integer, profit_loss: :integer, profit_loss_percent: :integer }
4
+ SEARCH_ATTRIBUTES = { start_date: :orders_created_from, end_date: :orders_created_till }
5
+ SORTABLE_ATTRIBUTES = []
6
+
7
+ class Result < Spree::Report::TimedResult
8
+ charts ProfitLossChart, ProfitLossPercentChart, SaleCostPriceChart
9
+
10
+ class Observation < Spree::Report::TimedObservation
11
+ observation_fields cost_price: 0, sale_price: 0, profit_loss: 0, profit_loss_percent: 0, promotion_discount: 0
12
+
13
+ def cost_price
14
+ @cost_price.to_f
15
+ end
16
+
17
+ def sale_price
18
+ @sale_price.to_f
19
+ end
20
+
21
+ def profit_loss
22
+ @profit_loss.to_f
23
+ end
24
+
25
+ def profit_loss_percent
26
+ return (profit_loss * 100 / cost_price).round(2) unless cost_price.zero?
27
+ 0.0
28
+ end
29
+
30
+ def promotion_discount
31
+ @promotion_discount.to_f
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ private def report_query
38
+ Spree::Report::QueryFragments
39
+ .from_union(order_with_line_items_grouped_by_time, promotions_grouped_by_time)
40
+ .group(*time_scale_columns_to_s)
41
+ .order(*time_scale_columns)
42
+ .project(
43
+ *time_scale_columns,
44
+ 'SUM(sale_price) as sale_price',
45
+ 'SUM(cost_price) as cost_price',
46
+ 'SUM(profit_loss) as profit_loss',
47
+ 'SUM(promotion_discount) as promotion_discount'
48
+ )
49
+ end
50
+
51
+ private def promotions_grouped_by_time
52
+ Spree::Report::QueryFragments
53
+ .from_subquery(promotion_adjustments_with_time)
54
+ .group(*time_scale_columns_to_s, 'sale_price', 'cost_price')
55
+ .order(*time_scale_columns)
56
+ .project(
57
+ *time_scale_columns,
58
+ '0 as sale_price',
59
+ '0 as cost_price',
60
+ 'SUM(promotion_discount) * -1 as profit_loss',
61
+ 'SUM(promotion_discount) as promotion_discount'
62
+ )
63
+ end
64
+
65
+ private def promotion_adjustments_with_time
66
+ Spree::Adjustment
67
+ .promotion
68
+ .where(created_at: reporting_period)
69
+ .select(
70
+ 'abs(amount) as promotion_discount',
71
+ *time_scale_selects('spree_adjustments')
72
+ )
73
+ end
74
+
75
+ private def order_with_line_items_grouped_by_time
76
+ order_with_line_items_ar = Arel::Table.new(:order_with_line_items)
77
+ zero = Arel::Nodes.build_quoted(0.0)
78
+ Spree::Report::QueryFragments
79
+ .from_subquery(order_with_line_items, as: :order_with_line_items)
80
+ .group(*time_scale_columns_to_s)
81
+ .order(*time_scale_columns)
82
+ .project(
83
+ *time_scale_columns,
84
+ Spree::Report::QueryFragments.if_null(Spree::Report::QueryFragments.sum(order_with_line_items_ar[:sale_price]), zero).as('sale_price'),
85
+ Spree::Report::QueryFragments.if_null(Spree::Report::QueryFragments.sum(order_with_line_items_ar[:cost_price]), zero).as('cost_price'),
86
+ Spree::Report::QueryFragments.if_null(Spree::Report::QueryFragments.sum(order_with_line_items_ar[:profit_loss]), zero).as('profit_loss'),
87
+ '0 as promotion_discount'
88
+ )
89
+ end
90
+
91
+ private def order_with_line_items
92
+ line_item_ar = Spree::LineItem.arel_table
93
+ Spree::Order
94
+ .where.not(completed_at: nil)
95
+ .where(created_at: reporting_period)
96
+ .joins(:line_items)
97
+ .group('spree_orders.id', *time_scale_columns_to_s)
98
+ .select(
99
+ *time_scale_selects('spree_orders'),
100
+ "spree_orders.item_total as sale_price",
101
+ "SUM(#{ Spree::Report::QueryFragments.if_null(line_item_ar[:cost_price], line_item_ar[:price]).to_sql } * spree_line_items.quantity) as cost_price",
102
+ "(spree_orders.item_total - SUM(#{ Spree::Report::QueryFragments.if_null(line_item_ar[:cost_price], line_item_ar[:price]).to_sql } * spree_line_items.quantity)) as profit_loss"
103
+ )
104
+ end
105
+
106
+ end
107
+ end