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,37 @@
1
+ class Spree::SalesPerformanceReport::ProfitLossChart
2
+ def initialize(result)
3
+ time_dim = result.time_dimension
4
+ @time_series = result.observations.collect(&time_dim)
5
+ @data = result.observations.collect(&:profit_loss)
6
+ end
7
+
8
+ def to_h
9
+ {
10
+ id: 'profit-loss',
11
+ json: {
12
+ title: {
13
+ useHTML: true,
14
+ text: "<span class='chart-title'>Profit/Loss</span><span class='fa fa-question-circle' data-toggle='tooltip' title='Track the profit or loss value'></span>"
15
+ },
16
+ xAxis: { categories: @time_series },
17
+ yAxis: {
18
+ title: { text: 'Value($)' }
19
+ },
20
+ legend: {
21
+ layout: 'vertical',
22
+ align: 'right',
23
+ verticalAlign: 'middle',
24
+ borderWidth: 0
25
+ },
26
+ series: [
27
+ {
28
+ name: 'Profit Loss',
29
+ tooltip: { valuePrefix: '$' },
30
+ data: @data
31
+ }
32
+ ]
33
+ }
34
+ }
35
+
36
+ end
37
+ end
@@ -0,0 +1,36 @@
1
+ class Spree::SalesPerformanceReport::ProfitLossPercentChart
2
+ def initialize(result)
3
+ time_dim = result.time_dimension
4
+ @time_series = result.observations.collect(&time_dim)
5
+ @data = result.observations.collect(&:profit_loss_percent)
6
+ end
7
+
8
+ def to_h
9
+ {
10
+ id: 'profit-loss-percent',
11
+ json: {
12
+ title: {
13
+ useHTML: true,
14
+ text: "<span class='chart-title'>Profit/Loss %</span><span class='fa fa-question-circle' data-toggle='tooltip' title='Track the profit or loss %age to create a projection'></span>"
15
+ },
16
+ xAxis: { categories: @time_series },
17
+ yAxis: {
18
+ title: { text: 'Percentage(%)' }
19
+ },
20
+ legend: {
21
+ layout: 'vertical',
22
+ align: 'right',
23
+ verticalAlign: 'middle',
24
+ borderWidth: 0
25
+ },
26
+ series: [
27
+ {
28
+ name: 'Profit Loss Percent(%)',
29
+ tooltip: { valueSuffix: '%' },
30
+ data: @data
31
+ }
32
+ ]
33
+ }
34
+ }
35
+ end
36
+ end
@@ -0,0 +1,48 @@
1
+ class Spree::SalesPerformanceReport::SaleCostPriceChart
2
+ def initialize(result)
3
+ time_dim = result.time_dimension
4
+ @time_series = result.observations.collect(&time_dim)
5
+ @sale_price = result.observations.collect(&:sale_price)
6
+ @cost_price = result.observations.collect(&:cost_price)
7
+ @promotion_discount = result.observations.collect(&:promotion_discount)
8
+ end
9
+
10
+ def to_h
11
+ {
12
+ id: 'sale-price',
13
+ json: {
14
+ chart: { type: 'column' },
15
+ title: {
16
+ useHTML: true,
17
+ text: "<span class='chart-title'>Sales Performance %</span><span class='fa fa-question-circle' data-toggle='tooltip' title='Compare the Selling price, cost price and promotional cost over a period of time'></span>"
18
+ },
19
+ xAxis: { categories: @time_series },
20
+ yAxis: {
21
+ title: { text: 'Value($)' }
22
+ },
23
+ tooltip: { valuePrefix: '$' },
24
+ legend: {
25
+ layout: 'vertical',
26
+ align: 'right',
27
+ verticalAlign: 'middle',
28
+ borderWidth: 0
29
+ },
30
+ series: [
31
+ {
32
+ name: 'Sale Price',
33
+ data: @sale_price
34
+ },
35
+ {
36
+ name: 'Cost Price',
37
+ data: @cost_price
38
+ },
39
+ {
40
+ name: 'Promotional Cost',
41
+ data: @promotion_discount
42
+ }
43
+ ]
44
+ }
45
+ }
46
+
47
+ end
48
+ end
@@ -0,0 +1,64 @@
1
+ module Spree
2
+ class SalesTaxReport < Spree::Report
3
+ HEADERS = { zone_name: :string, sales_tax: :integer }
4
+ SEARCH_ATTRIBUTES = { start_date: :taxation_from, end_date: :taxation_till }
5
+ SORTABLE_ATTRIBUTES = []
6
+
7
+ class Result < Spree::Report::TimedResult
8
+ charts MonthlySalesTaxComparisonChart
9
+
10
+ def build_empty_observations
11
+ super
12
+ @_zones = @results.collect { |r| r['zone_name'] }.uniq
13
+ @observations = @_zones.collect do |zone|
14
+ @observations.collect do |observation|
15
+ _d_observation = observation.dup
16
+ _d_observation.zone_name = zone
17
+ _d_observation.sales_tax = 0
18
+ _d_observation
19
+ end
20
+ end.flatten
21
+ end
22
+
23
+ class Observation < Spree::Report::TimedObservation
24
+ observation_fields [:zone_name, :sales_tax]
25
+
26
+ def describes?(result, time_scale)
27
+ (zone_name == result['zone_name']) && super
28
+ end
29
+
30
+ def sales_tax
31
+ @sales_tax.to_f
32
+ end
33
+ end
34
+ end
35
+
36
+
37
+ def report_query
38
+ Spree::Report::QueryFragments
39
+ .from_subquery(tax_adjustments)
40
+ .group(*time_scale_columns_to_s, 'zone_name')
41
+ .order(*time_scale_columns)
42
+ .project(
43
+ 'zone_name',
44
+ *time_scale_columns,
45
+ 'SUM(sales_tax) as sales_tax'
46
+ )
47
+ end
48
+
49
+ private def tax_adjustments
50
+ Spree::TaxRate
51
+ .joins(:adjustments)
52
+ .joins(:zone)
53
+ .where(spree_adjustments: { adjustable_type: 'Spree::LineItem' } )
54
+ .where(spree_adjustments: { created_at: reporting_period })
55
+ .select(
56
+ 'spree_adjustments.amount as sales_tax',
57
+ 'spree_zones.id as zone_id',
58
+ 'spree_zones.name as zone_name',
59
+ *time_scale_selects('spree_adjustments')
60
+ )
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,39 @@
1
+ class Spree::SalesTaxReport::MonthlySalesTaxComparisonChart
2
+ def initialize(result)
3
+ @time_dimension = result.time_dimension
4
+ @grouped_by_zone_name = result.observations.group_by(&:zone_name)
5
+ @time_series = []
6
+ if @grouped_by_zone_name.first.present?
7
+ @time_series = @grouped_by_zone_name.values.first.collect { |observation| observation.send(@time_dimension) }
8
+ end
9
+ @chart_series = @grouped_by_zone_name.map { |zone_name, observations| { type: 'column', name: zone_name, data: observations.collect(&:sales_tax) } }
10
+ end
11
+
12
+ def to_h
13
+ {
14
+ id: 'sale-tax',
15
+ json: {
16
+ chart: { type: 'column' },
17
+ title: {
18
+ useHTML: true,
19
+ text: "<span class='chart-title'>Monthly Sales Tax Comparison</span><span class='fa fa-question-circle' data-toggle='tooltip' title='Compare the Sales tax collected from different Zones'></span>"
20
+ },
21
+ xAxis: { categories: @time_series },
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: @chart_series
33
+ }
34
+ }
35
+
36
+ end
37
+
38
+
39
+ end
@@ -0,0 +1,89 @@
1
+ module Spree
2
+ class ShippingCostReport < Spree::Report
3
+ HEADERS = { name: :string, shipping_charge: :integer, revenue: :integer, shipping_cost_percentage: :integer }
4
+ SEARCH_ATTRIBUTES = { start_date: :start_date, end_date: :end_date }
5
+ SORTABLE_ATTRIBUTES = []
6
+
7
+ class Result < Spree::Report::TimedResult
8
+ charts ShippingCostDistributionChart
9
+
10
+ def build_empty_observations
11
+ super
12
+ @_shipping_methods = @results.collect { |r| r['name'] }.uniq
13
+ @observations = @_shipping_methods.collect do |shipping_method|
14
+ @observations.collect do |observation|
15
+ _d_observation = observation.dup
16
+ _d_observation.name = shipping_method
17
+ _d_observation.revenue = 0
18
+ _d_observation.shipping_charge = 0
19
+ _d_observation.shipping_cost_percentage = 0
20
+ _d_observation
21
+ end
22
+ end.flatten
23
+ end
24
+
25
+ class Observation < Spree::Report::TimedObservation
26
+ observation_fields [:name, :shipping_charge, :revenue, :shipping_cost_percentage]
27
+
28
+ def describes?(result, time_scale)
29
+ (name = result['name']) && super
30
+ end
31
+
32
+ def shipping_cost_percentage
33
+ ((@shipping_charge.to_f * 100) / @revenue.to_f).round(2)
34
+ end
35
+ end
36
+ end
37
+
38
+ def report_query
39
+ ar_shipping_methods = Arel::Table.new(:spree_shipping_methods)
40
+ ar_subquery_with_rates = Arel::Table.new(:shipment_with_rates)
41
+
42
+ Spree::Report::QueryFragments
43
+ .from_subquery(shipment_with_rates, as: 'shipment_with_rates')
44
+ .join(ar_shipping_methods)
45
+ .on(ar_shipping_methods[:id].eq(ar_subquery_with_rates[:shipping_method_id]))
46
+ .project(
47
+ *time_scale_columns,
48
+ ar_shipping_methods[:id],
49
+ 'revenue',
50
+ 'shipping_charge',
51
+ 'shipping_method_id',
52
+ 'name'
53
+ )
54
+ end
55
+
56
+ private def order_with_shipments
57
+ Spree::Order
58
+ .where.not(completed_at: nil)
59
+ .where(completed_at: reporting_period)
60
+ .joins(:shipments)
61
+ .select(
62
+ 'spree_shipments.id as shipment_id',
63
+ 'spree_orders.shipment_total as shipping_charge',
64
+ 'spree_orders.id as order_id',
65
+ 'spree_orders.total as order_total',
66
+ *time_scale_selects('spree_orders')
67
+ )
68
+ end
69
+
70
+ private def shipment_with_rates
71
+ ar_shipping_rates = Arel::Table.new(:spree_shipping_rates)
72
+ ar_subquery = Arel::Table.new(:results)
73
+
74
+ Spree::Report::QueryFragments.from_subquery(order_with_shipments)
75
+ .join(ar_shipping_rates)
76
+ .on(ar_shipping_rates[:shipment_id].eq(ar_subquery[:shipment_id]))
77
+ .where(ar_shipping_rates[:selected].eq(Arel::Nodes::Quoted.new(true)))
78
+ .group(*time_scale_columns, :shipping_method_id)
79
+ .order(*time_scale_columns)
80
+ .project(
81
+ *time_scale_columns,
82
+ 'shipping_method_id',
83
+ 'SUM(shipping_charge) as shipping_charge',
84
+ 'SUM(order_total) as revenue'
85
+ )
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,38 @@
1
+ class Spree::ShippingCostReport::ShippingCostDistributionChart
2
+
3
+
4
+ def initialize(result)
5
+ time_dimension = result.time_dimension
6
+ @grouped_by_shipping_method = result.observations.group_by(&:name)
7
+ @time_series = []
8
+ @time_series = @grouped_by_shipping_method.values.first.collect { |observation_value| observation_value.send(time_dimension) } if @grouped_by_shipping_method.first.present?
9
+ @result_series = @grouped_by_shipping_method.collect { |name, observations| { name: name, data: observations.collect(&:shipping_cost_percentage) } }
10
+ end
11
+
12
+ def to_h
13
+ {
14
+ id: 'shipping-cost-percentage-comparison',
15
+ json: {
16
+ chart: { type: 'spline' },
17
+ title: {
18
+ useHTML: true,
19
+ text: "<span class='chart-title'>Monthly Shipping Comparison</span><span class='fa fa-question-circle' data-toggle='tooltip' title='Compare the Shipping percentage (calculated on Revenue) among various shipment methods such as UPS, FedEx etc.'></span>"
20
+ },
21
+ xAxis: { categories: @time_series },
22
+ yAxis: {
23
+ title: { text: 'Percentage(%)' }
24
+ },
25
+ tooltip: { valueSuffix: '%' },
26
+ legend: {
27
+ layout: 'vertical',
28
+ align: 'right',
29
+ verticalAlign: 'middle',
30
+ borderWidth: 0
31
+ },
32
+ series: @result_series
33
+ }
34
+ }
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,50 @@
1
+ module Spree
2
+ class TrendingSearchReport < Spree::Report
3
+ DEFAULT_SORTABLE_ATTRIBUTE = :occurrences
4
+ HEADERS = { searched_term: :string, occurrences: :integer }
5
+ SEARCH_ATTRIBUTES = { start_date: :start_date, end_date: :end_date, keywords_cont: :keyword }
6
+ SORTABLE_ATTRIBUTES = [:occurrences]
7
+
8
+ def paginated?
9
+ true
10
+ end
11
+
12
+ class Result < Spree::Report::Result
13
+ charts FrequencyDistributionPieChart
14
+
15
+ class Observation < Spree::Report::Observation
16
+ observation_fields [:searched_term, :occurrences]
17
+ end
18
+ end
19
+
20
+ deeplink searched_term: { template: %Q{<a href='/products?utf8=%E2%9C%93&keywords={%# o['searched_term'] %}' target="_blank">{%# o['searched_term'] %}</a>} }
21
+
22
+ def paginated_report_query
23
+ report_query
24
+ .take(records_per_page)
25
+ .skip(current_page)
26
+ end
27
+
28
+ def record_count_query
29
+ Spree::Report::QueryFragments.from_subquery(report_query).project(Arel.star.count)
30
+ end
31
+
32
+ def report_query
33
+ Spree::Report::QueryFragments.from_subquery(searches)
34
+ .project("count(searched_term) as occurrences", "searched_term")
35
+ .group("searched_term")
36
+ end
37
+
38
+ private def searches
39
+ Spree::PageEvent
40
+ .where(activity: 'search')
41
+ .where(created_at: reporting_period)
42
+ .where(Spree::PageEvent.arel_table[:search_keywords].matches(keyword_search))
43
+ .select("search_keywords as searched_term")
44
+ end
45
+
46
+ private def keyword_search
47
+ search[:keywords_cont].present? ? "%#{ search[:keywords_cont] }%" : '%'
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,41 @@
1
+ class Spree::TrendingSearchReport::FrequencyDistributionPieChart
2
+ attr_accessor :chart_data
3
+
4
+ def initialize(result)
5
+ total_occurrences = result.observations.sum(&:occurrences).to_f
6
+ self.chart_data = result.observations.collect { |x| { name: x.searched_term, y: x.occurrences/total_occurrences } }
7
+ end
8
+
9
+ def to_h
10
+ {
11
+
12
+ name: 'trending-search',
13
+ json: {
14
+ chart: { type: 'pie' },
15
+ title: {
16
+ useHTML: true,
17
+ text: "<span class='chart-title'>Trending Search Keywords(Top 20)</span><span class='fa fa-question-circle' data-toggle='tooltip' title='Track the most trending keywords searched by users'></span>"
18
+ },
19
+ tooltip: {
20
+ pointFormat: 'Search %: <b>{point.percentage:.1f}%</b>'
21
+ },
22
+ plotOptions: {
23
+ pie: {
24
+ allowPointSelect: true,
25
+ cursor: 'pointer',
26
+ dataLabels: {
27
+ enabled: false
28
+ },
29
+ showInLegend: true
30
+ }
31
+ },
32
+ series: [
33
+ {
34
+ name: 'Hits',
35
+ data: chart_data
36
+ }
37
+ ]
38
+ }
39
+ }
40
+ end
41
+ end