spree_admin_insights 3.2.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 +6 -0
  4. data/LICENSE +26 -0
  5. data/README.md +96 -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/spree_admin_insights.js +2 -0
  9. data/app/assets/javascripts/spree/backend/spree_admin_insights/paginator.js +128 -0
  10. data/app/assets/javascripts/spree/backend/spree_admin_insights/report_loader.js +228 -0
  11. data/app/assets/javascripts/spree/backend/spree_admin_insights/searcher.js +103 -0
  12. data/app/assets/javascripts/spree/backend/spree_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/spree_admin_insights.js +2 -0
  15. data/app/assets/stylesheets/spree/backend/override_pdf.css +71 -0
  16. data/app/assets/stylesheets/spree/backend/spree_admin_insights.css +40 -0
  17. data/app/assets/stylesheets/spree/frontend/spree_admin_insights.css +4 -0
  18. data/app/controllers/spree/admin/insights_controller.rb +112 -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/models/spree/user_decorator.rb +3 -0
  25. data/app/overrides/add_insights_to_admin_side_menu.html.rb +5 -0
  26. data/app/reports/spree/best_selling_products_report.rb +42 -0
  27. data/app/reports/spree/cart_additions_report.rb +36 -0
  28. data/app/reports/spree/cart_removals_report.rb +36 -0
  29. data/app/reports/spree/cart_updations_report.rb +40 -0
  30. data/app/reports/spree/payment_method_transactions_conversion_rate_report.rb +74 -0
  31. data/app/reports/spree/payment_method_transactions_conversion_rate_report/payment_method_state_distribution_chart.rb +39 -0
  32. data/app/reports/spree/payment_method_transactions_report.rb +60 -0
  33. data/app/reports/spree/payment_method_transactions_report/payment_method_revenue_distribution_chart.rb +36 -0
  34. data/app/reports/spree/product_views_report.rb +46 -0
  35. data/app/reports/spree/product_views_to_cart_additions_report.rb +53 -0
  36. data/app/reports/spree/product_views_to_purchases_report.rb +54 -0
  37. data/app/reports/spree/promotional_cost_report.rb +84 -0
  38. data/app/reports/spree/promotional_cost_report/promotional_cost_chart.rb +37 -0
  39. data/app/reports/spree/promotional_cost_report/usage_count_chart.rb +41 -0
  40. data/app/reports/spree/report.rb +131 -0
  41. data/app/reports/spree/report/chart.rb +11 -0
  42. data/app/reports/spree/report/configuration.rb +40 -0
  43. data/app/reports/spree/report/date_slicer.rb +61 -0
  44. data/app/reports/spree/report/observation.rb +49 -0
  45. data/app/reports/spree/report/query_fragments.rb +45 -0
  46. data/app/reports/spree/report/query_time_scale.rb +19 -0
  47. data/app/reports/spree/report/result.rb +100 -0
  48. data/app/reports/spree/report/timed_observation.rb +47 -0
  49. data/app/reports/spree/report/timed_result.rb +48 -0
  50. data/app/reports/spree/returned_products_report.rb +37 -0
  51. data/app/reports/spree/sales_performance_report.rb +107 -0
  52. data/app/reports/spree/sales_performance_report/profit_loss_chart.rb +37 -0
  53. data/app/reports/spree/sales_performance_report/profit_loss_percent_chart.rb +36 -0
  54. data/app/reports/spree/sales_performance_report/sale_cost_price_chart.rb +48 -0
  55. data/app/reports/spree/sales_tax_report.rb +64 -0
  56. data/app/reports/spree/sales_tax_report/monthly_sales_tax_comparison_chart.rb +39 -0
  57. data/app/reports/spree/shipping_cost_report.rb +89 -0
  58. data/app/reports/spree/shipping_cost_report/shipping_cost_distribution_chart.rb +38 -0
  59. data/app/reports/spree/trending_search_report.rb +50 -0
  60. data/app/reports/spree/trending_search_report/frequency_distribution_pie_chart.rb +41 -0
  61. data/app/reports/spree/unique_purchases_report.rb +39 -0
  62. data/app/reports/spree/user_pool_report.rb +66 -0
  63. data/app/reports/spree/user_pool_report/distribution_column_chart.rb +65 -0
  64. data/app/reports/spree/users_not_converted_report.rb +48 -0
  65. data/app/reports/spree/users_who_recently_purchased_report.rb +69 -0
  66. data/app/services/spree/report_generation_service.rb +27 -0
  67. data/app/views/spree/admin/insights/_chart.html.erb +4 -0
  68. data/app/views/spree/admin/insights/download.pdf.erb +27 -0
  69. data/app/views/spree/admin/insights/index.html.erb +91 -0
  70. data/app/views/spree/admin/insights/search/_product_views_search.html.erb +13 -0
  71. data/app/views/spree/admin/insights/search/_search_form.html.erb +39 -0
  72. data/app/views/spree/admin/insights/search/_trending_searches_search.html.erb +13 -0
  73. data/app/views/spree/admin/insights/search/_users_not_converted_search.html.erb +13 -0
  74. data/app/views/spree/admin/insights/search/_users_who_have_not_purchased_recently_search.html.erb +13 -0
  75. data/app/views/spree/admin/insights/search/_users_who_recently_purchased_search.html.erb +13 -0
  76. data/app/views/spree/admin/shared/_insights_side_menu.html.erb +16 -0
  77. data/app/views/spree/admin/templates/insights/_paginator.template +9 -0
  78. data/app/views/spree/admin/templates/insights/_search.template +75 -0
  79. data/app/views/spree/admin/templates/insights/_show.template +48 -0
  80. data/app/views/spree/layouts/pdf.html.erb +9 -0
  81. data/bin/rails +7 -0
  82. data/config/initializers/assets.rb +1 -0
  83. data/config/initializers/mime_types.rb +2 -0
  84. data/config/initializers/wicked_pdf.rb +21 -0
  85. data/config/locales/en.yml +169 -0
  86. data/config/locales/pl.yml +164 -0
  87. data/config/routes.rb +6 -0
  88. data/lib/generators/spree_admin_insights/install/install_generator.rb +35 -0
  89. data/lib/generators/spree_admin_insights/install/spree_admin_insights.rb +22 -0
  90. data/lib/spree_admin_insights.rb +14 -0
  91. data/lib/spree_admin_insights/engine.rb +26 -0
  92. data/lib/spree_admin_insights/factories.rb +6 -0
  93. data/spec/spec_helper.rb +93 -0
  94. data/spree_admin_insights.gemspec +41 -0
  95. metadata +420 -0
@@ -0,0 +1,103 @@
1
+ //= require spree/backend/spree_admin_insights/paginator
2
+
3
+ function Searcher(inputs, reportLoader) {
4
+ this.$insightsTableList = inputs.insightsDiv;
5
+ this.$filters = inputs.filterDiv;
6
+ this.$quickSearchForm = this.$filters.find('#quick-search');
7
+ this.tableSorter = inputs.tableSorterObject;
8
+ this.reportLoader = reportLoader;
9
+ this.$filterForm = null;
10
+ this.$searchLabelsContainer = this.$filters.find('.table-active-filters');
11
+ }
12
+
13
+ Searcher.prototype.bindEvents = function(data) {
14
+ var _this = this;
15
+ this.$searchLabelsContainer.on("click", ".js-delete-filter", function() {
16
+ _this.$quickSearchForm[0].reset();
17
+ $(this).parent().hide();
18
+ });
19
+ };
20
+
21
+ Searcher.prototype.refreshSearcher = function($selectedInsight, data) {
22
+ var requestPath = $selectedInsight.data('url'),
23
+ _this = this;
24
+
25
+ _this.$filters.removeClass('hide');
26
+ _this.addSearchForm(data);
27
+ _this.setFormActions(_this.$quickSearchForm, requestPath);
28
+ _this.setFormActions(_this.$filterForm, requestPath);
29
+
30
+ _this.$filterForm.on('submit', function() {
31
+ var paginated = !_this.reportLoader.removePaginationButton.closest('span').hasClass('hide');
32
+ _this.addSearchStatus();
33
+ $.ajax({
34
+ type: "GET",
35
+ url: _this.$filterForm.attr('action'),
36
+ data: _this.$filterForm.serialize() + "&per_page=" + _this.reportLoader.pageSelector.find(':selected').attr('value') + '&paginate=' + paginated,
37
+ dataType: 'json',
38
+ success: function(data) {
39
+ _this.clearFormFields();
40
+ _this.reportLoader.requestUrl = this.url;
41
+ _this.populateInsightsData(data);
42
+ _this.reportLoader.paginatorObject.refreshPaginator(data);
43
+ }
44
+ });
45
+ return false;
46
+ });
47
+ };
48
+
49
+ Searcher.prototype.addSearchStatus = function () {
50
+ var filtersContainer = $(".js-filters");
51
+ filtersContainer.empty();
52
+ $(".js-filterable").each(function() {
53
+ var $this = $(this);
54
+
55
+ if ($this.val()) {
56
+ var ransack_value, filter;
57
+ var ransack_field = $this.attr("id");
58
+ var label = $('label[for="' + ransack_field + '"]');
59
+ if ($this.is("select")) {
60
+ ransack_value = $this.find('option:selected').text();
61
+ } else {
62
+ ransack_value = $this.val();
63
+ }
64
+
65
+ label = label.text() + ': ' + ransack_value;
66
+ filter = '<span class="js-filter label label-default" data-ransack-field="' + ransack_field + '">' + label + '<span class="icon icon-delete js-delete-filter"></span></span>';
67
+
68
+ filtersContainer.append(filter).show();
69
+ }
70
+ });
71
+ };
72
+
73
+ Searcher.prototype.addSearchForm = function(data) {
74
+ this.$filters.find('#table-filter').empty().append($(tmpl('search-tmpl', data)));
75
+ this.$filterForm = this.$filters.find('#filter-search');
76
+ this.$filters.find('.datepicker').datepicker({ dateFormat: 'yy-mm-dd' });
77
+ };
78
+
79
+ Searcher.prototype.setFormActions = function($form, path) {
80
+ $form.attr("method", "get");
81
+ $form.attr("action", path);
82
+ };
83
+
84
+ Searcher.prototype.populateInsightsData = function(data) {
85
+ this.reportLoader.populateInsightsData(data);
86
+ };
87
+
88
+ Searcher.prototype.clearFormFields = function() {
89
+ this.$filters.find('.filter-well').slideUp();
90
+ };
91
+
92
+ Searcher.prototype.fillFormFields = function(searchedFields) {
93
+ $.each(Object.keys(searchedFields), function() {
94
+ $('#search_' + this).val(searchedFields[this]);
95
+ });
96
+ this.addSearchStatus();
97
+ };
98
+
99
+ Searcher.prototype.clearSearchFields = function() {
100
+ this.$quickSearchForm[0].reset();
101
+ var filtersContainer = $(".js-filters");
102
+ filtersContainer.empty();
103
+ };
@@ -0,0 +1,47 @@
1
+ //= require spree/backend/jquery.tablesorter.min
2
+
3
+ function TableSorter(inputs) {
4
+ this.$insightsTableList = inputs.$insightsTable;
5
+ this.reportLoader = inputs.reportLoader;
6
+ this.paginatorDiv = inputs.paginatorDiv;
7
+ }
8
+
9
+ TableSorter.prototype.bindEvents = function() {
10
+ var _this = this;
11
+ this.$insightsTableList.on('click', '#admin-insight .sortable-link', function() {
12
+ event.preventDefault();
13
+ var currentPage = _this.paginatorDiv.find('li.active a').html() - 1,
14
+ paginated = !_this.reportLoader.removePaginationButton.closest('span').hasClass('hide'),
15
+ requestPath = $(event.target).attr('href') + '&' + $('#filter-search').serialize() + '&page=' + currentPage + "&per_page=" + _this.reportLoader.pageSelector.find(':selected').attr('value') + '&paginate=' + paginated;
16
+ _this.reportLoader.requestUrl = requestPath;
17
+
18
+ $.ajax({
19
+ type: 'GET',
20
+ url: requestPath,
21
+ dataType: 'json',
22
+ success: function(data) {
23
+ _this.populateInsightsData(data);
24
+ }
25
+ });
26
+ });
27
+ };
28
+
29
+ TableSorter.prototype.populateInsightsData = function(data) {
30
+ this.reportLoader.populateInsightsData(data);
31
+ };
32
+
33
+ TableSorter.prototype.fetchSortedAttribute = function() {
34
+ var attribute, sortOrder;
35
+ if (this.$insightsTableList.find('.asc').length) {
36
+ attribute = this.getSortedAttribute('asc');
37
+ sortOrder = 'asc';
38
+ } else if(this.$insightsTableList.find('.desc').length) {
39
+ attribute = this.getSortedAttribute('desc');
40
+ sortOrder = 'desc';
41
+ }
42
+ return [attribute, sortOrder];
43
+ };
44
+
45
+ TableSorter.prototype.getSortedAttribute = function(order) {
46
+ return this.$insightsTableList.find('.' + order).data('attribute');
47
+ };
@@ -0,0 +1,87 @@
1
+ /*
2
+ * JavaScript Templates 2.4.1
3
+ * https://github.com/blueimp/JavaScript-Templates
4
+ *
5
+ * Copyright 2011, Sebastian Tschan
6
+ * https://blueimp.net
7
+ *
8
+ * Licensed under the MIT license:
9
+ * http://www.opensource.org/licenses/MIT
10
+ *
11
+ * Inspired by John Resig's JavaScript Micro-Templating:
12
+ * http://ejohn.org/blog/javascript-micro-templating/
13
+ */
14
+
15
+ /*jslint evil: true, regexp: true, unparam: true */
16
+ /*global document, define */
17
+
18
+ (function ($) {
19
+ "use strict";
20
+ var tmpl = function (str, data) {
21
+ var f = !/[^\w\-\.:]/.test(str) ? tmpl.cache[str] = tmpl.cache[str] ||
22
+ tmpl(tmpl.load(str)) :
23
+ new Function(
24
+ tmpl.arg + ',tmpl',
25
+ "var _e=tmpl.encode" + tmpl.helper + ",_s='" +
26
+ str.replace(tmpl.regexp, tmpl.func) +
27
+ "';return _s;"
28
+ );
29
+ return data ? f(data, tmpl) : function (data) {
30
+ return f(data, tmpl);
31
+ };
32
+ };
33
+ tmpl.cache = {};
34
+ tmpl.load = function (id) {
35
+ return document.getElementById(id).innerHTML;
36
+ };
37
+ tmpl.regexp = /([\s'\\])(?!(?:[^{]|\{(?!%))*%\})|(?:\{%(=|#)([\s\S]+?)%\})|(\{%)|(%\})/g;
38
+ tmpl.func = function (s, p1, p2, p3, p4, p5) {
39
+ if (p1) { // whitespace, quote and backspace in HTML context
40
+ return {
41
+ "\n": "\\n",
42
+ "\r": "\\r",
43
+ "\t": "\\t",
44
+ " " : " "
45
+ }[p1] || "\\" + p1;
46
+ }
47
+ if (p2) { // interpolation: {%=prop%}, or unescaped: {%#prop%}
48
+ if (p2 === "=") {
49
+ return "'+_e(" + p3 + ")+'";
50
+ }
51
+ return "'+(" + p3 + "==null?'':" + p3 + ")+'";
52
+ }
53
+ if (p4) { // evaluation start tag: {%
54
+ return "';";
55
+ }
56
+ if (p5) { // evaluation end tag: %}
57
+ return "_s+='";
58
+ }
59
+ };
60
+ tmpl.encReg = /[<>&"'\x00]/g;
61
+ tmpl.encMap = {
62
+ "<" : "&lt;",
63
+ ">" : "&gt;",
64
+ "&" : "&amp;",
65
+ "\"" : "&quot;",
66
+ "'" : "&#39;"
67
+ };
68
+ tmpl.encode = function (s) {
69
+ /*jshint eqnull:true */
70
+ return (s == null ? "" : "" + s).replace(
71
+ tmpl.encReg,
72
+ function (c) {
73
+ return tmpl.encMap[c] || "";
74
+ }
75
+ );
76
+ };
77
+ tmpl.arg = "o";
78
+ tmpl.helper = ",print=function(s,e){_s+=e?(s==null?'':s):_e(s);}" +
79
+ ",include=function(s,d){_s+=tmpl(s,d);}";
80
+ if (typeof define === "function" && define.amd) {
81
+ define(function () {
82
+ return tmpl;
83
+ });
84
+ } else {
85
+ $.tmpl = tmpl;
86
+ }
87
+ }(this));
@@ -0,0 +1,2 @@
1
+ // Placeholder manifest file.
2
+ // the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/frontend/all.js'
@@ -0,0 +1,71 @@
1
+ .table {
2
+ border-collapse: collapse !important;
3
+ }
4
+
5
+ .table td {
6
+ background-color: #fff !important;
7
+ }
8
+
9
+ .table-bordered {
10
+ border: 1px solid #ddd;
11
+ }
12
+ .table-bordered > thead > tr > th,
13
+ .table-bordered > tbody > tr > th,
14
+ .table-bordered > tfoot > tr > th,
15
+ .table-bordered > thead > tr > td,
16
+ .table-bordered > tbody > tr > td,
17
+ .table-bordered > tfoot > tr > td {
18
+ border: 1px solid #ddd;
19
+ }
20
+ .table-bordered > thead > tr > th,
21
+ .table-bordered > thead > tr > td {
22
+ border-bottom-width: 2px;
23
+ }
24
+
25
+ table {
26
+ background-color: transparent;
27
+ }
28
+
29
+ th {
30
+ background-color: #f0f0f0 !important;
31
+ text-align: left;
32
+ }
33
+ .table {
34
+ width: 100%;
35
+ max-width: 100%;
36
+ margin-bottom: 20px;
37
+ }
38
+ .table > thead > tr > th,
39
+ .table > tbody > tr > th,
40
+ .table > tfoot > tr > th,
41
+ .table > thead > tr > td,
42
+ .table > tbody > tr > td,
43
+ .table > tfoot > tr > td {
44
+ padding: 8px;
45
+ line-height: 1.42857143;
46
+ vertical-align: top;
47
+ border-top: 1px solid #ddd;
48
+ }
49
+ .table > thead > tr > th {
50
+ vertical-align: bottom;
51
+ border-bottom: 2px solid #ddd;
52
+ }
53
+
54
+ .table > tbody + tbody {
55
+ border-top: 2px solid #ddd;
56
+ }
57
+ .table .table {
58
+ background-color: #fff;
59
+ }
60
+ .logo {
61
+ width: auto;
62
+ height: 20px;
63
+ }
64
+
65
+ .spree-head {
66
+ padding: 20px 0px 20px 20px;
67
+ width: 100%;
68
+ max-width: 100%;
69
+ background: #162e40;
70
+ height: 50;
71
+ }
@@ -0,0 +1,40 @@
1
+ /*
2
+ Placeholder manifest file.
3
+ the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/backend/all.css'
4
+ */
5
+
6
+ .page-select {
7
+ padding: 0px 6px 0px 6px;
8
+ }
9
+
10
+ #report-container .filter-well {
11
+ margin: 5px 0px 10px 0px;
12
+ }
13
+
14
+ .select-box-slide {
15
+ margin-left: 3px;
16
+ }
17
+
18
+ #page-selector {
19
+ margin-right: 5px;
20
+ }
21
+
22
+ #table-helpers .col-md-2 {
23
+ padding-left: 3px;
24
+ }
25
+
26
+ .chart-title {
27
+ padding-right: 5px;
28
+ }
29
+
30
+ .sortable-arrow {
31
+ color: #2F85D5;
32
+ }
33
+
34
+ .loss-indicator {
35
+ color: red;
36
+ }
37
+
38
+ .profit-indicator {
39
+ color: green;
40
+ }
@@ -0,0 +1,4 @@
1
+ /*
2
+ Placeholder manifest file.
3
+ the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/frontend/all.css'
4
+ */
@@ -0,0 +1,112 @@
1
+ module Spree
2
+ module Admin
3
+ class InsightsController < Spree::Admin::BaseController
4
+ before_action :ensure_report_exists, :set_default_pagination, only: [:show, :download]
5
+ before_action :set_reporting_period, only: [:index, :show, :download]
6
+ before_action :load_reports, only: [:index, :show]
7
+
8
+ def index
9
+ respond_to do |format|
10
+ format.html
11
+ format.json { render json: {} }
12
+ end
13
+ end
14
+
15
+ def show
16
+ report = ReportGenerationService.generate_report(@report_name, params.merge(@pagination_hash))
17
+
18
+ @report_data = shared_data.merge(report.to_h)
19
+ respond_to do |format|
20
+ format.html { render :index }
21
+ format.json { render json: @report_data }
22
+ end
23
+ end
24
+
25
+ def download
26
+ @report = ReportGenerationService.generate_report(@report_name, params.merge(@pagination_hash))
27
+
28
+ respond_to do |format|
29
+ format.csv do
30
+ send_data ReportGenerationService.download(@report),
31
+ filename: "#{ @report_name.to_s }.csv"
32
+ end
33
+ format.xls do
34
+ send_data ReportGenerationService.download(@report, { col_sep: "\t" }),
35
+ filename: "#{ @report_name.to_s }.xls"
36
+ end
37
+ format.text do
38
+ send_data ReportGenerationService.download(@report),
39
+ filename: "#{ @report_name.to_s }.txt"
40
+ end
41
+ format.pdf do
42
+ render pdf: "#{ @report_name.to_s }",
43
+ disposition: 'attachment',
44
+ layout: 'spree/layouts/pdf.html'
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+ def ensure_report_exists
51
+ @report_name = params[:id].to_sym
52
+ unless ReportGenerationService.report_exists?(get_report_category, @report_name)
53
+ redirect_to admin_insights_path, alert: Spree.t(:not_found, scope: [:reports])
54
+ end
55
+ end
56
+
57
+ def load_reports
58
+ @reports = ReportGenerationService.reports_for_category(get_report_category)
59
+ end
60
+
61
+ def shared_data
62
+ {
63
+ current_page: params[:page] || 0,
64
+ report_category: params[:report_category],
65
+ request_path: request.path,
66
+ url: request.url,
67
+ searched_fields: params[:search],
68
+ }
69
+ end
70
+
71
+ def get_report_category
72
+ params[:report_category] = if params[:report_category]
73
+ params[:report_category].to_sym
74
+ else
75
+ session[:report_category].try(:to_sym) || ReportGenerationService.default_report_category
76
+ end
77
+ session[:report_category] = params[:report_category]
78
+ end
79
+
80
+ def set_reporting_period
81
+ if params[:search].present?
82
+ if params[:search][:start_date] == ""
83
+ # When clicking on 'x' to remove the filter
84
+ params[:search][:start_date] = nil
85
+ else
86
+ params[:search][:start_date] = params[:search][:start_date] || session[:search_start_date]
87
+ end
88
+ if params[:search][:end_date] == ""
89
+ params[:search][:end_date] = nil
90
+ else
91
+ params[:search][:end_date] = params[:search][:end_date].presence || session[:search_end_date]
92
+ end
93
+ else
94
+ params[:search] = {}
95
+ params[:search][:start_date] = session[:search_start_date]
96
+ params[:search][:end_date] = session[:search_end_date]
97
+ end
98
+ session[:search_start_date] = params[:search][:start_date]
99
+ session[:search_end_date] = params[:search][:end_date]
100
+ end
101
+
102
+ def set_default_pagination
103
+ @pagination_hash = { paginate: false }
104
+ unless params[:paginate] == 'false'
105
+ @pagination_hash[:paginate] = true
106
+ @pagination_hash[:records_per_page] = params[:per_page].try(:to_i) || Spree::Config[:records_per_page]
107
+ @pagination_hash[:offset] = params[:page].to_i * @pagination_hash[:records_per_page]
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end