shopify_dashboard_plus 0.0.3 → 0.0.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5eec23dd7be2f6961002521056ee0b78c7b16f47
4
- data.tar.gz: 9476b25596ca4545fe7700488a54322210408195
3
+ metadata.gz: f9cb5e599bcb7d985aecabdaa34959e0bd0ac515
4
+ data.tar.gz: a6ddcb38acf2a363c585ab864b93b62003bd3c5d
5
5
  SHA512:
6
- metadata.gz: cd5f93de262d3d3a86f49c12d3d8800e6d398372188b6e565d9f6fbbb2ff65185b9dc61a2cb25726b77b1b036ca12106497d174aed3396d48ae1e849bffce3c0
7
- data.tar.gz: 8b1526903f8805beaecbb761dae82156c4eecd9fc200950488e7bc74ae2f96cf6448dc05e2bfb703d6cd5299f23e5dc70bebf4adcb95cf83191f32f116e5a660
6
+ metadata.gz: 698165b5c02d12e72464cf9030dea2ceaeb06ce302d04f442f24a56a71d6b8c165ce9c70c914b1d9a44a51a63c463b71371eb924f46ff2543ccdc671506cc469
7
+ data.tar.gz: b5e7b6ab374f757618c6f2b8e9026918b0dbe50d7537159554987f527446430af7d578d4388f408f2c11af70242d0eee0e6e2e27b4101f37c23ff40ed109b6cb
data/README.md CHANGED
@@ -20,6 +20,8 @@ Choose the interval over which to get data and see it displayed as:
20
20
  * Proportion of Items Sold Per Price Point
21
21
  * Number of Items Sold Per Price Point
22
22
  * Revenue per Price Point
23
+ * Total Savings per Discount Code
24
+ * Number of Uses per Discount Code
23
25
 
24
26
  *Countries*
25
27
  * Proportion of Sales per Country
@@ -39,16 +41,14 @@ Choose the interval over which to get data and see it displayed as:
39
41
  ## Usage
40
42
  * To install manually:
41
43
  * `git clone https://github.com/at1as/Shopify_Dashboard_Plus.git`
42
- * To install via the gem:
44
+ * To install using the gem:
43
45
  * `gem install shopify_dashboard_plus`
44
- * Note: Gem build will generally trail the repo by days-to-weeks
45
46
  * For dependencies see:
46
47
  * `shopify_dashboard_plus.gemfile`
47
48
  * Retrieve `API Key`, `Password` and `Shop Name` for your store from Shopify Admin
48
49
  * Run (key, password & name can be passed as environment variables, or later thorugh the UI):
49
50
  * `SHP_KEY="my_key" SHP_PWD="my_password" SHP_NAME="my_shop" ./lib/shopify_dashboard_plus.rb`
50
51
 
51
- ## TODO
52
+ ## Notes
53
+ * Tested and developed with Ruby 2.1.2 on Mac OS 10.10
52
54
 
53
- * Not all floats render with a two-digit precision
54
- * Limited to 250 results
@@ -5,19 +5,25 @@ require 'tilt/erubis'
5
5
  require 'shopify_api'
6
6
  require 'uri'
7
7
  require 'chartkick'
8
+ require_relative 'shopify_dashboard_plus/helpers'
9
+ require_relative 'shopify_dashboard_plus/report'
10
+ require_relative 'shopify_dashboard_plus/version'
8
11
 
9
12
  configure do
10
- set :public_dir, File.expand_path('../../public', __FILE__)
11
- set :views, File.expand_path('../../views', __FILE__)
13
+ set :public_dir, "#{__dir__}/../public"
14
+ set :views, "#{__dir__}/../views"
12
15
 
13
16
  $connected ||= false
14
- $flash ||= nil
15
17
  $metrics ||= false
18
+ $flash ||= nil
16
19
 
17
- HELP = "Set global environment variables before calling script, or call with ENV variables: \
18
- \t\nExample: SHP_KEY=\"<shop_key>\" SHP_PWD=\"<shop_password>\" SHP_NAME=\"<shop_name>\" ./lib/shopify-dashboard.rb\n"
20
+ HELP = <<-END
21
+ Set global variables through the WebUI or call with the correct ENV variables:
22
+ Example: SHP_KEY=\"<shop_key>\" SHP_PWD=\"<shop_password>\" SHP_NAME=\"<shop_name>\" ./lib/shopify-dashboard.rb
23
+ END
19
24
 
20
- # Connect if Environment variables were set
25
+ # If Environment variables were set connect to Shopify Admin immediately
26
+ # Refuse to start server if variables are passed but incorrect
21
27
  if ENV["SHP_KEY"] && ENV["SHP_PWD"] && ENV["SHP_NAME"]
22
28
  API_KEY = ENV["SHP_KEY"]
23
29
  PASSWORD = ENV["SHP_PWD"]
@@ -30,263 +36,33 @@ configure do
30
36
  $shop_name = SHOP_NAME
31
37
  $connected = true
32
38
  rescue Exception => e
33
- puts "\nFailed to connect using provided credentials...(Exception: #{e}\n #{HELP}"
39
+ puts "\nFailed to connect using provided credentials...(Exception: #{e})\n #{HELP}"
34
40
  exit
35
41
  end
36
42
  end
37
- end
38
-
39
-
40
- helpers do
41
- include Rack::Utils
42
- alias_method :h, :escape_html
43
-
44
- ## Connection & Setup Helpers
45
- # Bind to Shopify Store
46
- def set_connection(key, pwd, name)
47
- begin
48
- shop_url = "https://#{key}:#{pwd}@#{name}.myshopify.com/admin"
49
- ShopifyAPI::Base.site = shop_url
50
- shop = ShopifyAPI::Shop.current
51
- $shop_name = name
52
- connection_open
53
- rescue
54
- connection_closed
55
- end
56
- end
57
-
58
- def connection_closed
59
- $connected = false
60
- end
61
-
62
- def connection_open
63
- $connected = true
64
- end
65
-
66
- def connected?
67
- $connected
68
- end
69
-
70
- def shop_name
71
- $shop_name
72
- end
73
-
74
-
75
- ## Generic Helpers
76
- def hash_to_list(unprocessed_hash)
77
- return_list = []
78
- unprocessed_hash.each do |key, value|
79
- return_list.append([key, value])
80
- end
81
- end
82
-
83
- def date_today
84
- DateTime.now.strftime('%Y-%m-%d')
85
- end
86
-
87
- # Validate User Date is valid and start date <= end date
88
- def validate_date_range(from, to)
89
- interval_start = DateTime.parse(from) rescue nil
90
- interval_end = DateTime.parse(to) rescue nil
91
43
 
92
- if interval_start && interval_end
93
- if interval_start <= interval_end
94
- return true
95
- else
96
- return false
97
- end
98
- end
99
-
100
- false
101
- end
102
-
103
-
104
- ## Metrics Helpers
105
- def get_total_revenue(orders)
106
- revenue = orders.collect{|order| order.total_price.to_f }.inject(:+).round(2) rescue 0
107
- revenue ||= 0
108
- end
109
44
 
110
- def get_daily_revenues(start_date, end_date, orders)
111
- # Create hash entry for total interval over which to inspect sales
112
- revenue_per_day = {}
113
- days = DateTime.parse(end_date).mjd - DateTime.parse(start_date).mjd
114
- (0..days).each{ |day| revenue_per_day[(DateTime.parse(end_date) - day).strftime("%Y-%m-%d")] = 0 }
115
-
116
- # Retreive orders between start and end date (up to 50)
117
- revenue = orders.collect{|order| [order.created_at, order.total_price.to_f]}
118
-
119
- # Filter order details into daily totals and return
120
- revenue.each do |sale|
121
- revenue_per_day[DateTime.parse(sale[0]).strftime('%Y-%m-%d')] += sale[1]
45
+ # When adding 44.95.round(2) + 940.6.round(2) the precision of the result will be 985.5500000000001
46
+ # In a sample of 100_000_000_000 entries, the precision will round up cents
47
+ # Since all numbers are currency, the plus method will trim to two decimals
48
+ # Numbers returned as: 44, 44.5, or 44.51
49
+ class Fixnum
50
+ def plus(amount)
51
+ result = self + (amount.to_f rescue 0)
52
+ result.round(2)
122
53
  end
123
- revenue_per_day
124
54
  end
125
55
 
126
- def hash_to_graph_format(sales)
127
-
128
- # ChartKick requires a strange format to build graphs. For instance, an array of
129
- # {:name => <item_name>, :data => [[<customer_id>, <item_price>], [<customer_id>, <item_price>]]}
130
- # places <customer_id> on the independent (x) axis, and stacks item each item by y-axis by price
131
-
132
- # This hash will return repeated values (i.e., :data => [["item 1", 6], ["item 1", 6]])
133
- # ChartKick will ignore repeated entries. Use `format_hash_for_stacked_graph_repeat` to merge entries before submission
134
-
135
- name_hash = sales.collect{|sale| {:name => sale[:name], :data => []}}.uniq
136
-
137
- sales.collect do |old_hash|
138
- name_hash.collect do |new_hash|
139
- if old_hash[:name] == new_hash[:name]
140
- new_hash[:data].push(old_hash[:data])
141
- end
142
- end
56
+ class Float
57
+ def plus(amount)
58
+ result = self + (amount.to_f rescue 0)
59
+ result.round(2)
143
60
  end
144
-
145
- name_hash
146
61
  end
62
+ end
147
63
 
64
+ helpers ApplicationHelpers
148
65
 
149
- def hash_to_graph_format_merge(sales)
150
-
151
- # ChartKick requires an annoying format to build graphs. For instance, an array of entries formated as
152
- # { :name => <item_name>, :data => [[<customer_id>, <item_price>], [<customer_id>, <item_price>]] }
153
- # places <customer_id> on the independent (x) axis, and stacks item each item by y-axis by price
154
-
155
- name_hash = sales.collect{|sale| {:name => sale[:name], :data => []}}.uniq
156
-
157
- sales.collect do |old_hash|
158
- name_hash.collect do |new_hash|
159
- if old_hash[:name] == new_hash[:name]
160
- new_hash[:data].push(old_hash[:data])
161
- end
162
- end
163
- end
164
-
165
- # name_hash may contain repeated values (i.e., :data => [["item 1", 6], ["item 1", 6]])
166
- # ChartKick will ignore repeated entries, so they need to be consolidated
167
- # i.e., :data => [["item1", 12]]
168
-
169
- name_hash.each_with_index do |item, index|
170
- consolidated_data = Hash.new(0)
171
- item[:data].each do |purchase_entry|
172
- consolidated_data[purchase_entry[0]] += purchase_entry[1]
173
- end
174
- name_hash[index][:data] = hash_to_list(consolidated_data)
175
- end
176
-
177
- name_hash
178
- end
179
-
180
-
181
- def get_detailed_revenue_metrics(start_date, end_date = DateTime.now)
182
- desired_fields = ["total_price", "created_at", "billing_address", "currency", "line_items", "customer", "referring_site"]
183
- revenue_metrics = ShopifyAPI::Order.find(:all, :params => { :created_at_min => start_date,
184
- :created_at_max => end_date,
185
- :page => 1,
186
- :limit => 250,
187
- :fields => desired_fields })
188
-
189
- # Revenue
190
- total_revenue = get_total_revenue(revenue_metrics)
191
- avg_revenue = (total_revenue/(DateTime.parse(end_date).mjd - DateTime.parse(start_date).mjd + 1)).round(2) rescue "N/A"
192
- daily_revenue = get_daily_revenues(start_date, end_date, revenue_metrics)
193
-
194
- # Countries & Currencies
195
- currencies = Hash.new(0)
196
- sales_per_country = Hash.new(0)
197
- revenue_per_country = []
198
- revenue_per_country_uniq = []
199
-
200
- # Products
201
- products = Hash.new(0)
202
- revenue_per_product = Hash.new(0)
203
-
204
- # Prices
205
- prices = Hash.new(0)
206
- revenue_per_price_point = Hash.new(0)
207
-
208
- # Customers
209
- customers = []
210
- customer_sales = []
211
- customer_sales_uniq = []
212
-
213
- # Referrals
214
- referring_pages = Hash.new(0)
215
- referring_sites = Hash.new(0)
216
- revenue_per_referral_page = Hash.new(0)
217
- revenue_per_referral_site = Hash.new(0)
218
-
219
- revenue_metrics.each do |order|
220
-
221
- if order.attributes['currency']
222
- currencies[order.currency] += 1
223
- end
224
- if order.attributes['billing_address']
225
- sales_per_country[order.billing_address.country] += 1
226
- end
227
- if order.attributes['referring_site']
228
- if order.attributes['referring_site'].empty?
229
- referring_pages['None'] += 1
230
- referring_sites['None'] += 1
231
- else
232
- host = URI(order.referring_site).host.downcase
233
- host = host.start_with?('www.') ? host[4..-1] : host
234
- page = order.referring_site
235
- page = page.start_with?('http://') ? page[7..-1] : page
236
- page = page.start_with?('https://') ? page[8..-1] : page
237
- referring_pages[page] += 1
238
- referring_sites[host] += 1
239
- end
240
- order.line_items.each do |line_item|
241
- if order.attributes['referring_site'].empty?
242
- revenue_per_referral_page['None'] += line_item.price.to_f.round(2) rescue 0
243
- revenue_per_referral_site['None'] += line_item.price.to_f.round(2) rescue 0
244
- else
245
- host = URI(order.referring_site).host
246
- host = host.start_with?('www.') ? host[4..-1] : host
247
- page = order.referring_site
248
- page = page.start_with?('http://') ? page[7..-1] : page
249
- page = page.start_with?('https://') ? page[8..-1] : page
250
- revenue_per_referral_site[host] += line_item.price.to_f.round(2) rescue 0
251
- revenue_per_referral_page[page] += line_item.price.to_f.round(2) rescue 0
252
- end
253
- end
254
- end
255
-
256
- order.line_items.each do |line_item|
257
- products[line_item.title] += 1
258
- prices[line_item.price] += 1
259
- revenue_per_price_point[line_item.price] += line_item.price.to_f.round(2) rescue 0
260
- revenue_per_product[line_item.title] += line_item.price.to_f.round(2) rescue 0
261
-
262
- revenue_per_country.push({:name => line_item.title, :data => [order.billing_address.country, line_item.price.to_f]})
263
- customer_sales.push({:name => line_item.title, :data => [order.customer.id.to_s, line_item.price.to_f]})
264
- end
265
-
266
- customer_sales_uniq = hash_to_graph_format(customer_sales)
267
- revenue_per_country_uniq = hash_to_graph_format_merge(revenue_per_country)
268
- end
269
-
270
- metrics = { :currencies => currencies,
271
- :sales_per_country => sales_per_country,
272
- :revenue_per_country => revenue_per_country_uniq,
273
- :products => products,
274
- :prices => (prices.sort_by{|x,y| x.to_f }.to_h rescue {}),
275
- :customer_sales => customer_sales_uniq,
276
- :referring_sites => (referring_sites.sort().to_h rescue {}),
277
- :referring_pages => (referring_pages.sort().to_h rescue {}),
278
- :revenue_per_referral_site => (revenue_per_referral_site.sort().to_h rescue {}),
279
- :revenue_per_referral_page => (revenue_per_referral_page.sort().to_h rescue {}),
280
- :total_revenue => total_revenue,
281
- :average_revenue => avg_revenue,
282
- :daily_revenue => daily_revenue,
283
- :revenue_per_product => revenue_per_product,
284
- :revenue_per_price_point => (revenue_per_price_point.sort_by{|x,y| x.to_f }.to_h rescue {})
285
- }
286
-
287
- return metrics
288
- end
289
- end
290
66
 
291
67
  before do
292
68
  $flash = nil
@@ -300,11 +76,12 @@ get '/' do
300
76
  # If no start date is set, default to match end date
301
77
  # If no date parameters are set, default both to today
302
78
  @today = date_today
303
-
304
79
  from = (params[:from] if not params[:from].empty? rescue nil) || (params[:to] if not params[:to].empty? rescue nil) || @today
305
80
  to = (params[:to] if not params[:to].empty? rescue nil) || @today
306
81
 
307
- if validate_date_range(from, to)
82
+ to = @today if to > @today
83
+
84
+ if date_range_valid?(from, to)
308
85
  @metrics = get_detailed_revenue_metrics(from, to)
309
86
  $metrics = true
310
87
  else
@@ -331,7 +108,7 @@ end
331
108
 
332
109
  post '/disconnect' do
333
110
  API_KEY, API_PWD, SHP_NAME = "", "", ""
334
- connection_closed
111
+ close_connection
335
112
 
336
113
  erb :connect
337
114
  end
@@ -0,0 +1,202 @@
1
+ module ApplicationHelpers
2
+
3
+ include Rack::Utils
4
+ alias_method :h, :escape_html
5
+
6
+ DESIRED_FIELDS = [
7
+ "total_price",
8
+ "created_at",
9
+ "billing_address",
10
+ "currency",
11
+ "line_items",
12
+ "customer",
13
+ "referring_site",
14
+ "discount_codes"
15
+ ]
16
+
17
+ ## Connection & Setup Helpers
18
+
19
+ def set_connection(key, pwd, name)
20
+ shop_url = "https://#{key}:#{pwd}@#{name}.myshopify.com/admin"
21
+ ShopifyAPI::Base.site = shop_url
22
+ shop = ShopifyAPI::Shop.current
23
+ $shop_name = name
24
+ open_connection
25
+ rescue
26
+ close_connection
27
+ end
28
+
29
+ def close_connection
30
+ $connected = false
31
+ end
32
+
33
+ def open_connection
34
+ $connected = true
35
+ end
36
+
37
+ def connected?
38
+ $connected
39
+ end
40
+
41
+ def shop_name
42
+ $shop_name
43
+ end
44
+
45
+
46
+ ## Generic Helpers
47
+
48
+ def date_today
49
+ DateTime.now.strftime('%Y-%m-%d')
50
+ end
51
+
52
+ def days(num)
53
+ num.to_i == 1 ? "#{num} Day" : "#{num} Days"
54
+ end
55
+
56
+ def strip_protocol(page)
57
+ page.sub(/\Ahttps?:\/\//, "")
58
+ end
59
+
60
+ def get_host(site)
61
+ URI(site).host.downcase.sub(/\Awww\./, "")
62
+ end
63
+
64
+ def date_range_valid?(from, to)
65
+ DateTime.parse(from) <= DateTime.parse(to)
66
+ rescue ArgumentError
67
+ false
68
+ end
69
+
70
+
71
+ ## Metrics Helpers
72
+
73
+ def display_as_currency(value)
74
+ ShopifyAPI::Shop.current.money_with_currency_format.gsub("{{amount}}", value.to_s)
75
+ rescue
76
+ 'N/A'
77
+ end
78
+
79
+ def get_date_range(first, last)
80
+ DateTime.parse(last).mjd - DateTime.parse(first).mjd
81
+ end
82
+
83
+ def get_total_revenue(orders)
84
+ totals = orders.map { |order| order.total_price.to_f }
85
+ totals.inject(:+).round(2)
86
+ rescue
87
+ 0
88
+ end
89
+
90
+ def get_average_revenue(total_revenue, duration)
91
+ (total_revenue/duration).round(2)
92
+ rescue
93
+ 'N/A'
94
+ end
95
+
96
+ def get_daily_revenues(start_date, end_date, orders)
97
+ # Create hash entry for every day within interval over which to inspect sales
98
+ revenue_per_day = {}
99
+ days = get_date_range(start_date, end_date)
100
+ (0..days).each{ |day| revenue_per_day[(DateTime.parse(end_date) - day).strftime("%Y-%m-%d")] = 0 }
101
+
102
+ # Retreive array of ActiveRecord::Collections, each containing orders between the start and end date
103
+ order_details = orders.map{ |order| [order.created_at, order.total_price.to_f] }
104
+
105
+ # Filter order details into daily totals and return
106
+ order_details.each do |(date, total)|
107
+ day_index = DateTime.parse(date).strftime('%Y-%m-%d')
108
+ revenue_per_day[day_index] = revenue_per_day[day_index].plus(total)
109
+ end
110
+ revenue_per_day
111
+ end
112
+
113
+ def hash_to_graph_format(sales, merge_results: false)
114
+
115
+ # ChartKick requires a strange format to build graphs. For instance, an array of
116
+ # {:name => <item_name>, :data => [[<customer_id>, <item_price>], [<customer_id>, <item_price>]]}
117
+ # places <customer_id> on the independent (x) axis, and stacks each item (item_name) on the y-axis by price (item_price)
118
+
119
+ name_hash = sales.map{ |sale| {:name => sale[:name], :data => []} }.uniq
120
+
121
+ sales.map do |old_hash|
122
+ name_hash.map do |new_hash|
123
+ if old_hash[:name] == new_hash[:name]
124
+ new_hash[:data].push(old_hash[:data])
125
+ end
126
+ end
127
+ end
128
+
129
+ # This hash will return repeated values (i.e., :data => [["item 1", 6], ["item 1", 6]])
130
+ # ChartKick will ignore repeated entries, so the totals need to be merged
131
+ # i.e., :data => [["item1", 12]]
132
+ if merge_results
133
+ name_hash.each_with_index do |item, index|
134
+ consolidated_data = Hash.new(0)
135
+ item[:data].each do |purchase_entry|
136
+ consolidated_data[purchase_entry[0]] = consolidated_data[purchase_entry[0]].plus(purchase_entry[1])
137
+ end
138
+ name_hash[index][:data] = consolidated_data.to_a
139
+ end
140
+ end
141
+
142
+ name_hash
143
+ end
144
+
145
+ # Return order query parameters hash
146
+ def order_parameters_paginate(start_date, end_date, page)
147
+ {
148
+ :created_at_min => start_date + " 0:00",
149
+ :created_at_max => end_date + " 23:59:59",
150
+ :limit => 250,
151
+ :page => page,
152
+ :fields => DESIRED_FIELDS
153
+ }
154
+ end
155
+
156
+
157
+ # Return array of ActiveRecord::Collections, each containing up to :limit (250) orders
158
+ # Continue to query next page until less than :limit orders are returned, indicating no next pages with orders matching query
159
+ def get_list_of_orders(start_date, end_date)
160
+
161
+ # Get first 250 results matching query
162
+ params = order_parameters_paginate(start_date, end_date, 1)
163
+ revenue_metrics = [ShopifyAPI::Order.find(:all, :params => params)]
164
+
165
+ # If the amount of results equal to the limit (250) were returned, pass the query on to the next page (orders 251 to 500)
166
+ while revenue_metrics.last.length == 250
167
+ params = order_parameters_paginate(start_date, end_date, revenue_metrics.length + 1)
168
+ revenue_metrics << ShopifyAPI::Order.find(:all, :params => params)
169
+ end
170
+
171
+ revenue_metrics.flat_map{ |orders| orders.map{ |order| order }}
172
+ end
173
+
174
+
175
+ def get_detailed_revenue_metrics(start_date, end_date = DateTime.now)
176
+
177
+ order_list = get_list_of_orders(start_date, end_date)
178
+
179
+ # Revenue
180
+ total_revenue = get_total_revenue(order_list)
181
+ duration = get_date_range(start_date, end_date) + 1
182
+ avg_revenue = get_average_revenue(total_revenue, duration)
183
+ daily_revenue = get_daily_revenues(start_date, end_date, order_list)
184
+ max_daily_revenue = daily_revenue.max_by{ |k,v| v }[1]
185
+
186
+ # Retrieve Metrics
187
+ sales_report = ShopifyDashboardPlus::SalesReport.new(order_list).to_h
188
+ revenue_report = ShopifyDashboardPlus::RevenueReport.new(order_list).to_h
189
+ traffic_report = ShopifyDashboardPlus::TrafficReport.new(order_list).to_h
190
+ discounts_report = ShopifyDashboardPlus::DiscountReport.new(order_list).to_h
191
+ metrics = {
192
+ :total_revenue => total_revenue,
193
+ :average_revenue => avg_revenue,
194
+ :daily_revenue => daily_revenue,
195
+ :max_daily_revenue => max_daily_revenue,
196
+ :duration => duration
197
+ }
198
+
199
+ [sales_report, revenue_report, traffic_report, discounts_report, metrics].inject(&:merge)
200
+ end
201
+
202
+ end
@@ -0,0 +1,194 @@
1
+ require_relative 'helpers'
2
+
3
+ module ShopifyDashboardPlus
4
+
5
+ class Report
6
+ include ApplicationHelpers
7
+
8
+ def initialize(orders)
9
+ @orders = orders
10
+ @line_items = orders.flat_map{ |order| order.line_items.map { |line_item| line_item }}
11
+ end
12
+ end
13
+
14
+
15
+ class SalesReport < Report
16
+ def sales_per_country
17
+ sales_per_country = Hash.new(0)
18
+
19
+ @orders.each do |order|
20
+ sales_per_country[order.billing_address.country] += 1 if order.attributes['billing_address']
21
+ end
22
+ sales_per_country
23
+ end
24
+
25
+ def sales_per_product
26
+ sales_per_product = Hash.new(0)
27
+
28
+ @line_items.each do |item|
29
+ sales_per_product[item.title] += 1
30
+ end
31
+
32
+ sales_per_product
33
+ end
34
+
35
+ def sales_per_customer
36
+ customer_sales = []
37
+ @orders.each do |order|
38
+ order.line_items.each do |item|
39
+ customer_name = "#{order.customer.first_name} #{order.customer.last_name} (#{order.customer.email})"
40
+ customer_sales.push({ :name => item.title,
41
+ :data => [customer_name, item.price.to_f]})
42
+ end
43
+ end
44
+ hash_to_graph_format(customer_sales)
45
+ end
46
+
47
+ def sales_per_price_point
48
+ sales_per_price_point = Hash.new(0)
49
+
50
+ @line_items.each do |item|
51
+ sales_per_price_point[item.price] += 1
52
+ end
53
+ sales_per_price_point.sort_by{ |x,y| x.to_f }.to_h rescue {}
54
+ end
55
+
56
+ def number_of_sales
57
+ @orders.length
58
+ end
59
+
60
+ def currencies_per_sale
61
+ currencies = Hash.new(0)
62
+
63
+ @orders.each do |order|
64
+ currencies[order.currency] += 1 if order.attributes['currency']
65
+ end
66
+ currencies
67
+ end
68
+
69
+ def to_h
70
+ {
71
+ :currencies_per_sale => currencies_per_sale,
72
+ :sales_per_country => sales_per_country,
73
+ :sales_per_price => sales_per_price_point,
74
+ :sales_per_product => sales_per_product,
75
+ :sales_per_customer => sales_per_customer,
76
+ :number_of_sales => number_of_sales
77
+ }
78
+ end
79
+ end
80
+
81
+
82
+ class RevenueReport < Report
83
+ def revenue_per_country
84
+ revenue_per_country = []
85
+ @orders.each do |order|
86
+ order.line_items.each do |item|
87
+ revenue_per_country.push({:name => item.title,
88
+ :data => [order.billing_address.country, item.price.to_f]})
89
+ end
90
+ end
91
+ hash_to_graph_format(revenue_per_country, merge_results: true)
92
+ end
93
+
94
+ def revenue_per_price_point
95
+ revenue_per_price_point = Hash.new(0)
96
+ @line_items.each do |item|
97
+ revenue_per_price_point[item.price] = revenue_per_price_point[item.price].plus(item.price)
98
+ end
99
+ revenue_per_price_point.sort_by{ |x,y| x.to_f }.to_h rescue {}
100
+ end
101
+
102
+ def revenue_per_product
103
+ revenue_per_product = Hash.new(0.0)
104
+ @line_items.each do |item|
105
+ revenue_per_product[item.title] = revenue_per_product[item.title].plus(item.price)
106
+ end
107
+ revenue_per_product
108
+ end
109
+
110
+ def to_h
111
+ {
112
+ :revenue_per_country => revenue_per_country,
113
+ :revenue_per_product => revenue_per_product,
114
+ :revenue_per_price_point => revenue_per_price_point,
115
+ }
116
+ end
117
+ end
118
+
119
+
120
+ class DiscountReport < Report
121
+
122
+ def discount_usage
123
+ discount_value, discount_used = Hash.new(0.0), Hash.new(0)
124
+
125
+ @orders.each do |order|
126
+ if order.attributes['discount_codes']
127
+ order.discount_codes.each do |discount_code|
128
+ discount_value[discount_code.code] = discount_value[discount_code.code].plus(discount_code.amount)
129
+ discount_used[discount_code.code] += 1
130
+ end
131
+ end
132
+ end
133
+ {
134
+ :discount_savings => discount_value,
135
+ :discount_quantity => discount_used
136
+ }
137
+ end
138
+
139
+ def to_h
140
+ discount_usage
141
+ end
142
+ end
143
+
144
+
145
+ class TrafficReport < Report
146
+
147
+ def number_of_referrals
148
+ referring_sites, referring_pages = Hash.new(0), Hash.new(0)
149
+
150
+ @orders.each do |order|
151
+ if order.attributes['referring_site'].empty?
152
+ referring_pages['None'] += 1
153
+ referring_sites['None'] += 1
154
+ else
155
+ host = get_host(order.referring_site)
156
+ page = strip_protocol(order.referring_site)
157
+ referring_pages[page] += 1
158
+ referring_sites[host] += 1
159
+ end
160
+ end
161
+ {
162
+ :referral_sites => (referring_sites.sort().to_h rescue {}),
163
+ :referral_pages => (referring_pages.sort().to_h rescue {})
164
+ }
165
+ end
166
+
167
+ def traffic_revenue
168
+ revenue_per_referral_page, revenue_per_referral_site = Hash.new(0.0), Hash.new(0.0)
169
+
170
+ @orders.each do |order|
171
+ order.line_items.each do |item|
172
+ if order.attributes['referring_site'].empty?
173
+ revenue_per_referral_page['None'] = revenue_per_referral_page['None'].plus(item.price)
174
+ revenue_per_referral_site['None'] = revenue_per_referral_site['None'].plus(item.price)
175
+ else
176
+ host = get_host(order.referring_site)
177
+ page = strip_protocol(order.referring_site)
178
+ revenue_per_referral_site[host] = revenue_per_referral_site[host].plus(item.price)
179
+ revenue_per_referral_page[page] = revenue_per_referral_page[page].plus(item.price)
180
+ end
181
+ end
182
+ end
183
+ {
184
+ :revenue_per_referral_site => revenue_per_referral_site.sort().to_h,
185
+ :revenue_per_referral_page => revenue_per_referral_page.sort().to_h
186
+ }
187
+ end
188
+
189
+ def to_h
190
+ traffic_revenue.merge(number_of_referrals)
191
+ end
192
+ end
193
+
194
+ end
@@ -1,3 +1,3 @@
1
1
  module ShopifyDashboardPlus
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.4"
3
3
  end
@@ -1,3 +1,6 @@
1
+ body{
2
+ background-color:#444;
3
+ }
1
4
  .graph-container{
2
5
  width:100%;
3
6
  text-align:center;
@@ -5,7 +8,7 @@
5
8
  .graph-border{
6
9
  border:1px solid #e3e3e3;
7
10
  border-radius:4px;
8
- background-color:#f5f5f5;
11
+ background-color:#fff;
9
12
  padding-right:10px;
10
13
  padding-left:10px;
11
14
  margin-left:auto;
@@ -14,6 +17,26 @@
14
17
  margin-bottom:10px;
15
18
  max-width:1200px;
16
19
  }
20
+ .graph-seperator{
21
+ margin-top:0px;
22
+ margin-bottom:0px;
23
+ margin-left:40px;
24
+ margin-right:40px;
25
+ }
26
+ .graph-metrics{
27
+ margin-left:20px;
28
+ margin-right:20px;
29
+ text-align:center;
30
+ display:inline-block;
31
+ }
32
+ .graph-metric{
33
+ margin-left:20px;
34
+ margin-right:20px;
35
+ display:inline-block;
36
+ }
37
+ .grey{
38
+ color:#666;
39
+ }
17
40
  h3.section-divider{
18
41
  background-color: #666;
19
42
  color: white;
@@ -21,13 +44,32 @@ h3.section-divider{
21
44
  margin-bottom:15px;
22
45
  margin-left: -10px;
23
46
  margin-right: -10px;
24
- padding-bottom: 5px;
25
- padding-top: 5px;
47
+ padding-bottom: 10px;
48
+ padding-top: 10px;
26
49
  text-align: center;
27
50
  }
51
+ h3.money{
52
+ margin-top:0px;
53
+ color:#337ab7;
54
+ }
55
+ h3{
56
+ font-weight:300;
57
+ }
58
+ h4{
59
+ color:#555;
60
+ }
61
+ h4.graph-heading{
62
+ font-weight:400;
63
+ font-size:22px;
64
+ }
28
65
  .flash{
29
66
  color:red;
30
67
  }
68
+ .date-input{
69
+ width:100px;
70
+ margin-left:auto;
71
+ margin-right:auto;
72
+ }
31
73
 
32
74
  /*
33
75
  // NOTE: media queries not working will chartkick,
@@ -4,11 +4,12 @@ lib = File.expand_path('../lib', __FILE__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
 
6
6
  require 'date'
7
+ require 'shopify_dashboard_plus/version'
7
8
 
8
9
  Gem::Specification.new do |spec|
9
10
  spec.name = "shopify_dashboard_plus"
10
11
  spec.date = Date.today.to_s
11
- spec.version = "0.0.3"
12
+ spec.version = ShopifyDashboardPlus::VERSION
12
13
  spec.authors = ["Jason Willems"]
13
14
  spec.email = ["jason@willems.ca"]
14
15
  spec.summary = "Extended dashboard for shopify admin"
data/views/layout.erb CHANGED
@@ -5,8 +5,9 @@
5
5
  <script>
6
6
  var Chartkick = {"language": "en"};
7
7
  </script>
8
- <script src="http://www.google.com/jsapi"></script>
9
- <script src="js/chartkick.min.js"></script>
8
+ <script src="http://www.google.com/jsapi"></script> <!-- Chart library dependency for chartkick -->
9
+ <script src="js/chartkick.min.js"></script> <!-- Front end chart library -->
10
+ <script src="/js/spin.min.js"></script> <!-- Spinner Animation -->
10
11
  </head>
11
12
  <body>
12
13
  <%= yield %>
data/views/report.erb CHANGED
@@ -1,16 +1,15 @@
1
1
  <div id="table_bounds" style="margin-left:10px; margin-right:10px; margin-bottom:40px">
2
2
  <div class="well" style="text-align:center; max-width:600px; margin-left:auto; margin-right:auto; margin-top:30px">
3
3
  <h2 style="margin-top:10px; margin-bottom:20px">Shopify Dashboard Plus</h2>
4
- <hr>
5
4
  <% if $flash %><p class="flash"><%= $flash %></p><% end %>
6
5
  <div style="margin-bottom:20px;">
7
6
  Retrieve metrics over the following period <% if shop_name %>for: <br/><i><b><%= shop_name %>.myshopify.com/admin</b></i><% end %>
8
7
  </div>
9
8
  <form class="form-inline" id="set-date" method="get" action="/">
10
9
  <h4 style="margin-bottom:10px">
11
- <input id="from" name="from" class="form-control form-field-small" value="<%= h(params[:from]) %>" pattern="^[1-2][0-9]{3}-[0-3][0-9]-[0-3][0-9]$" placeholder="<%= @today %>">
10
+ <input id="from" name="from" class="form-control form-field-small date-input" value="<%= h(params[:from]) %>" pattern="^[1-2][0-9]{3}-[0-3][0-9]-[0-3][0-9]$" placeholder="<%= @today %>">
12
11
  to
13
- <input id="to" name="to" class="form-control form-field-small" value="<%= h(params[:to]) %>" pattern="^[1-2][0-9]{3}-[0-3][0-9]-[0-3][0-9]$"% placeholder="<%= @today %>">
12
+ <input id="to" name="to" class="form-control form-field-small date-input" value="<%= h(params[:to]) %>" pattern="^[1-2][0-9]{3}-[0-3][0-9]-[0-3][0-9]$"% placeholder="<%= @today %>">
14
13
  </h4>
15
14
  <input type="submit" class="btn btn-primary" value="Get Data" style="width:200px">
16
15
  <form>
@@ -23,52 +22,120 @@
23
22
  <h3 class="section-divider">Sales</h3>
24
23
  <div class="graph-container">
25
24
  <div class="graph-border">
26
- <div id="daily_revenue" onclick="fullscreen_graph(this)" style="text-align:center"><h4>Daily Sales</h4></div>
25
+ <h4 class="graph-heading">
26
+ Daily Sales
27
+ </h4>
28
+ <hr class="graph-seperator">
27
29
  <%= column_chart @metrics[:daily_revenue], library: {hAxis: {direction: -1}} %>
28
- <div style="text-align:center"><h5>Total Sales: <%= @metrics[:total_revenue] %></h5></div>
29
- <div style="text-align:center"><h5>Average Per Day: <%= @metrics[:average_revenue] %></h5></div>
30
+ <div class="graph-metrics">
31
+ <div class="graph-metric">
32
+ <h5 class="grey">Duration</h5>
33
+ <h3 class="money"><%= days(@metrics[:duration]) %></h3>
34
+ </div>
35
+ <div class="graph-metric">
36
+ <h5 class="grey">Number of Sales</h5>
37
+ <h3 class="money"><%= @metrics[:number_of_sales] %></h3>
38
+ </div>
39
+ <div class="graph-metric">
40
+ <h5 class="grey">Total Revenue</h5>
41
+ <h3 class="money"><%= display_as_currency @metrics[:total_revenue] %></h3>
42
+ </div>
43
+ <div class="graph-metric">
44
+ <h5 class="grey">Average Daily Revenue</h5>
45
+ <h3 class="money"><%= display_as_currency @metrics[:average_revenue] %></h3>
46
+ </div>
47
+ <div class="graph-metric">
48
+ <h5 class="grey">Max Daily Revenue</h5>
49
+ <h3 class="money"><%= display_as_currency @metrics[:max_daily_revenue] %></h3>
50
+ </div>
51
+ </div>
30
52
  </div>
53
+
31
54
  <div class="graph-border">
32
- <div style="text-align:center"><h4>Proportion of Sales per Product</h4></div>
33
- <%= pie_chart @metrics[:products] %>
55
+ <h4 class="graph-heading">
56
+ Proportion of Sales per Product
57
+ </h4>
58
+ <hr class="graph-seperator">
59
+ <%= pie_chart @metrics[:sales_per_product] %>
34
60
  </div>
35
61
 
36
62
  <div class="graph-border">
37
- <div style="text-align:center"><h4>Number of Sales per Product</h4></div>
38
- <%= column_chart @metrics[:products] %>
63
+ <h4 class="graph-heading">
64
+ Number of Sales per Product
65
+ </h4>
66
+ <hr class="graph-seperator">
67
+ <%= column_chart @metrics[:sales_per_product] %>
39
68
  </div>
69
+
40
70
  <div class="graph-border">
41
- <div style="text-align:center"><h4>Revenue per Product</h4></div>
71
+ <h4 class="graph-heading">
72
+ Revenue per Product
73
+ </h4>
74
+ <hr class="graph-seperator">
42
75
  <%= column_chart @metrics[:revenue_per_product] %>
43
76
  </div>
44
77
  </div>
45
78
 
46
79
 
47
- <h3 class="section-divider">Prices</h3>
80
+ <h3 class="section-divider">Prices &amp; Discounts</h3>
48
81
  <div class="graph-container">
49
82
  <div class="graph-border">
50
- <div style="text-align:center"><h4>Proportion of Items Sold Per Price Point</h4></div>
51
- <%= pie_chart @metrics[:prices] %>
83
+ <h4 class="graph-heading">
84
+ Proportion of Items Sold Per Price Point
85
+ </h4>
86
+ <hr class="graph-seperator">
87
+ <%= pie_chart @metrics[:sales_per_price] %>
52
88
  </div>
89
+
53
90
  <div class="graph-border">
54
- <div style="text-align:center"><h4>Number of Items Sold Per Price Point</h4></div>
55
- <%= column_chart @metrics[:prices] %>
91
+ <h4 class="graph-heading">
92
+ Number of Items Sold Per Price Point
93
+ </h4>
94
+ <hr class="graph-seperator">
95
+ <%= column_chart @metrics[:sales_per_price] %>
56
96
  </div>
97
+
57
98
  <div class="graph-border">
58
- <div style="text-align:center"><h4>Revenue per Price Point</h4></div>
99
+ <h4 class="graph-heading">
100
+ Revenue per Price Point
101
+ </h4>
102
+ <hr class="graph-seperator">
59
103
  <%= column_chart @metrics[:revenue_per_price_point] %>
60
104
  </div>
105
+
106
+ <div class="graph-border">
107
+ <h4 class="graph-heading">
108
+ Total Savings per Discount Code
109
+ </h4>
110
+ <hr class="graph-seperator">
111
+ <%= column_chart @metrics[:discount_savings] %>
112
+ </div>
113
+
114
+ <div class="graph-border">
115
+ <h4 class="graph-heading">
116
+ Number of Uses per Discount Code
117
+ </h4>
118
+ <hr class="graph-seperator">
119
+ <%= column_chart @metrics[:discount_quantity] %>
120
+ </div>
61
121
  </div>
62
122
 
63
123
 
64
124
  <h3 class="section-divider">Countries</h3>
65
125
  <div class="graph-container">
66
126
  <div class="graph-border">
67
- <div style="text-align:center"><h4>Proportion of Sales per Country</h4></div>
127
+ <h4 class="graph-heading">
128
+ Proportion of Sales per Country
129
+ </h4>
130
+ <hr class="graph-seperator">
68
131
  <%= pie_chart @metrics[:sales_per_country] %>
69
132
  </div>
133
+
70
134
  <div class="graph-border">
71
- <div style="text-align:center"><h4>Revenue per Country</h4></div>
135
+ <h4 class="graph-heading">
136
+ Revenue per Country
137
+ </h4>
138
+ <hr class="graph-seperator">
72
139
  <%= column_chart @metrics[:revenue_per_country], stacked: true %>
73
140
  </div>
74
141
  </div>
@@ -77,8 +144,11 @@
77
144
  <h3 class="section-divider">Currencies</h3>
78
145
  <div class="graph-container">
79
146
  <div class="graph-border">
80
- <div style="text-align:center"><h4>Currencies Used per Purchase</h4></div>
81
- <%= pie_chart @metrics[:currencies] %>
147
+ <h4 class="graph-heading">
148
+ Currencies Used per Purchase
149
+ </h4>
150
+ <hr class="graph-seperator">
151
+ <%= pie_chart @metrics[:currencies_per_sale] %>
82
152
  </div>
83
153
  </div>
84
154
 
@@ -86,8 +156,11 @@
86
156
  <h3 class="section-divider">Customers</h3>
87
157
  <div class="graph-container">
88
158
  <div class="graph-border">
89
- <div style="text-align:center"><h4>Purchases per Customer</h4></div>
90
- <%= column_chart @metrics[:customer_sales], stacked: true %>
159
+ <h4 class="graph-heading">
160
+ Purchases per Customer
161
+ </h4>
162
+ <hr class="graph-seperator">
163
+ <%= column_chart @metrics[:sales_per_customer], stacked: true %>
91
164
  </div>
92
165
  </div>
93
166
 
@@ -95,23 +168,48 @@
95
168
  <h3 class="section-divider">Traffic Metrics</h3>
96
169
  <div class="graph-container">
97
170
  <div class="graph-border">
98
- <div style="text-align:center"><h4>Referrals per Site</h4></div>
99
- <%= column_chart @metrics[:referring_sites] %>
171
+ <h4 class="graph-heading">
172
+ Referrals per Site
173
+ </h4>
174
+ <hr class="graph-seperator">
175
+ <%= column_chart @metrics[:referral_sites] %>
100
176
  </div>
177
+
101
178
  <div class="graph-border">
102
- <div style="text-align:center"><h4>Referrals per Site Pages</h4></div>
103
- <%= column_chart @metrics[:referring_pages] %>
179
+ <h4 class="graph-heading">
180
+ Referrals per Site Pages
181
+ </h4>
182
+ <hr class="graph-seperator">
183
+ <%= column_chart @metrics[:referral_pages] %>
104
184
  </div>
185
+
105
186
  <div class="graph-border">
106
- <div style="text-align:center"><h4>Revenue Per Referral Site</h4></div>
187
+ <h4 class="graph-heading">
188
+ Revenue Per Referral Site
189
+ </h4>
190
+ <hr class="graph-seperator">
107
191
  <%= column_chart @metrics[:revenue_per_referral_site] %>
108
192
  </div>
193
+
109
194
  <div class="graph-border">
110
- <div style="text-align:center"><h4>Revenue Per Referral Site Page</h4></div>
195
+ <h4 class="graph-heading">
196
+ Revenue Per Referral Site Page
197
+ </h4>
198
+ <hr class="graph-seperator">
111
199
  <%= column_chart @metrics[:revenue_per_referral_page] %>
112
200
  </div>
113
201
  </div>
114
202
 
203
+
204
+ <div style="margin-top:30px; margin-left:auto; margin-right:auto; text-align:center; color:#999">
205
+ Generated <%= Time.now %>
206
+ </div>
207
+
208
+ <script type="text/javascript">
209
+ window.onload = function(){
210
+ //document.getElementById("loading").style.display = "none"
211
+ }
212
+ </script>
115
213
  <!--
116
214
  <div id="fullscreen-graph" style="z-index:10; margin-left:auto; margin-right:auto;"></div>
117
215
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shopify_dashboard_plus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Willems
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-24 00:00:00.000000000 Z
11
+ date: 2015-06-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sinatra
@@ -147,7 +147,9 @@ files:
147
147
  - README.md
148
148
  - bin/shopify_dashboard_plus.rb
149
149
  - lib/shopify_dashboard_plus.rb
150
- - lib/version/shopify_dashboard_plus.rb
150
+ - lib/shopify_dashboard_plus/helpers.rb
151
+ - lib/shopify_dashboard_plus/report.rb
152
+ - lib/shopify_dashboard_plus/version.rb
151
153
  - public/css/bootstrap.min.css
152
154
  - public/css/common.css
153
155
  - public/js/chartkick.min.js