module FiordlandPacks
export metrics, metrics_plot
using Airtable, CSV, DataFrames, Dates, HTTP, Indicators, JSON3, PrettyTables, UnicodePlots, VegaLite
"""
"""
function metrics(date::String, invoices::Int)
@show make_visitor_plot(date)
@show build_page_rank_table(date)
@show north_star(date)
@show airtable_sales_for_period(date)
@show sales(date, invoices) #number of invoices sent MYOB, PayPal
end
# update csv manually because script is sometimes wrong for now
function metrics_plot()
if Sys.isapple()
file = "/Users/david/julia/FiordlandPacks/images_and_tables/FiordlandPacks.csv"
else
file = "/media/david/SSD1/FiordlandPacks/images_and_tables/FiordlandPacks.csv"
end
df = DataFrame(CSV.File(file))
transform!(df, [:Shopify_Sales, :Invoice_Sales] => (a, b) -> a .+ b)
select!(df, :Week, :Contact_Submit, :Pack_Builder_Clicks, :Checkouts_Created, :Customer_Actions,
:Shopify_Sales, :Invoice_Sales, :Shopify_Sales_Invoice_Sales_function => "Sales", :Google_Spend)
table = pretty_table(last(df, 12); title="Metrics",
header=["Week", "Contact", "Builder", "Checkout", "ACTIONS", "Shopify", "Invoice", "Sales", "Google"],
display_size=(30, 300))
df2 = Base.stack(df, [:Customer_Actions, :Sales, :Google_Spend])
transform!(df2, :Week => ByRow(x -> Dates.format(x, "yyyy-mm-dd")))
rename!(df2, :variable => "Metric", :value => "Number")
df2 |>
@vlplot(
:line,
x = :Week,
y = :Number,
color = :Metric,
title = "Metrics Plot",
width = 400,
height = 400
) |>
if Sys.isapple()
save("/Users/david/julia/FiordlandPacks/images_and_tables/metrics_fiordland_packs.png")
else
save("/media/david/SSD1/FiordlandPacks/images_and_tables/metrics_fiordland_packs.png")
end
return table
end
### PLAUSIBLE
# Needs ENV["Plausible"] = "" in startup.jl
function query_plausible(url::String)
headers = ["Authorization" => "Bearer $(ENV["Plausible"])"]
data = JSON3.read(HTTP.get(url, headers).body)
return data.results
end
# +5 day because of moving average, need refactor later.
function get_traffic(end_date::Dates.Date, days::Int, SITE_ID::String="fiordlandpacks.nz")::DataFrame
start_date = end_date - Dates.Day(days + 5)
url = "https://plausible.io/api/v1/stats/timeseries?site_id=$SITE_ID&period=custom&date=$(dts(start_date)),$(dts(end_date))"
df = DataFrame(query_plausible(url))
return df
end
"""
make_visitor_plot(date::String)
Takes a date in string form yy-mm-dd, number of days defaults to 30.
Returns a unicode plot of unique site visits, compared to 1 year ago.
"""
function make_visitor_plot(date::String, days::Int=30)
current = std(date)
if current == Dates.today()
current -= Dates.Day(1)
end
last_year = current - Dates.Year(1)
c = get_traffic(current, days)[!, 2] |> x -> Indicators.sma(x, n=7) |> x -> last(x, days) |> x -> round.(x, digits=1)
p = get_traffic(last_year, days)[!, 2] |> x -> Indicators.sma(x, n=7) |> x -> last(x, days) |> x -> round.(x, digits=1)
plt = lineplot(collect(-days:-1), [c p], color=[:blue :yellow], title="Unique Visitors: 7 day moving average", name=["Last $days Days", "Last Year"], xlabel="Day", ylabel="Visitors", canvas=DotCanvas, border=:ascii)
vcat(DataFrame("Days" => collect(-days:-1), "Visits" => c, "Period" => "Last $days Days"), DataFrame("Days" => collect(-days:-1), "Visits" => p, "Period" => "Previous Year")) |>
@vlplot(
:line,
x = :Days,
y = :Visits,
color = :Period,
title = "Unique Visitors: 7 Day Moving Average",
width = 400,
height = 400
) |>
if Sys.isapple()
save("/Users/david/julia/FiordlandPacks/images_and_tables/visits_fiordland_packs.png")
else
save("/media/david/SSD1/FiordlandPacks/images_and_tables/visits_fiordland_packs.png")
end
return plt
end
# Date to string formatter
function dts(date::Dates.Date)::String
return Dates.format(date, "yyyy-mm-dd")
end
# String to date formatter
function std(date::String)::Dates.Date
return Date("$date", dateformat"y-m-d")
end
function get_page_rank(end_date::Dates.Date, days::Int, SITE_ID::String="fiordlandpacks.nz")::DataFrame
start_date = end_date - Dates.Day(days)
url = "https://plausible.io/api/v1/stats/breakdown?site_id=$SITE_ID&period=custom&date=$(dts(start_date)),$(dts(end_date))&property=event:page&limit=10"
df = DataFrame(query_plausible(url))
rename!(df, :page => "$(days)_days")
select!(df, Not(:visitors))
return df
end
"""
build_page_rank_table(date::String)
Takes a date in string form yy-mm-dd.
Returns a pretty table of top 10 pages for 7, 30, 365 days.
Make sure repl window is wide enough
"""
function build_page_rank_table(date::String)
current = Date("$date", dateformat"y-m-d")
if current == Dates.today()
current -= Dates.Day(1)
end
week = get_page_rank(current, 7)
month = get_page_rank(current, 30)
year = get_page_rank(current, 365)
table = hcat(week, month, year)
return pretty_table(table; title="Page Rank", header=["week", "month", "year"])
end
# "Outbound+Link:+Click", "Contact+Submit"
function get_custom_events(end_date::Dates.Date, event::String, days::Int=7, SITE_ID::String="fiordlandpacks.nz")::@NamedTuple{Event::String, Number::Int64}
start_date = end_date - Dates.Day(days)
url = "https://plausible.io/api/v1/stats/breakdown?site_id=$SITE_ID&period=custom&date=$(dts(start_date)),$(dts(end_date))&property=event:props:method&filters=event:name%3D%3D$event"
df = DataFrame(query_plausible(url))
e = replace(event, "+" => " ") |> x -> titlecase(x)
isempty(df) ? (return (Event=e, Number=0)) : (return (Event=e, Number=first(df.visitors)))
end
# "thank_you", "checkouts"
function get_pageview_events(end_date::Dates.Date, event::String, days::Int=7, SITE_ID::String="fiordlandpacks.nz")::@NamedTuple{Event::String, Number::Int64}
start_date = end_date - Dates.Day(days)
url = "https://plausible.io/api/v1/stats/breakdown?site_id=$SITE_ID&period=custom&date=$(dts(start_date)),$(dts(end_date))&property=event:props:method&filters=event:page==/**$event**"
df = DataFrame(query_plausible(url))
e = replace(event, "_" => " ") |> x -> titlecase(x)
isempty(df) ? (return (Event=e, Number=0)) : (return (Event=e, Number=first(df.visitors)))
end
function north_star(date::String, days::Int=7)
d = std(date)
contact = get_custom_events(d, "Contact+Submit", days)
pack_builders = get_custom_events(d, "Outbound+Link:+Click", days)
checkouts = get_pageview_events(d, "checkouts", days)
df = DataFrame([contact, pack_builders, checkouts])
push!(df, ["Total" sum(df.Number)])
df[2, 1] = "Pack Builders"
return pretty_table(df; title="North Star Metric", header=["Event", "Number"], body_hlines=[3])
end
function sales(date::String, invoices_sent::Int, days::Int=7)
d = std(date)
shopify = get_pageview_events(d, "thank_you", days)
inv = (Event="Invoice", Number=invoices_sent)
df = DataFrame([shopify, inv])
push!(df, ["Total" sum(df.Number)])
df[1, 1] = "Shopify"
return pretty_table(df; title="Sales", header=["", "Number"], body_hlines=[2])
end
### AIRTABLE
base = (key="appt7SR7DsTsh22zO", name="Pack Builders")
tables = [
#(key="tblZzSmPq7LhdHUFk", name="25l Strong"),
#(key="tbltssUMDKNkjC8ld", name="35l"),
(key="tbldrQLd5cO2D1XEb", name="45l"),
(key="tbl1JARaob1EhEv2l", name="55l"),
(key="tbl8NXUqwPniIOxbM", name="65l") #=,
(key="tblp5cmBqyD4SY9zv", name="AC50"),
(key="tblKCK6N2BapDupSF", name="AC60"),
(key="tblSLbNuFaOb0Qorp", name="AC70"),
(key="tbl9Etx9No0cPn11A", name="Waist Bag")=#
]
function table_sales_for_period(table::String, end_date::Dates.Date, days::Int=7, base::String="appt7SR7DsTsh22zO")::DataFrame
start_date = end_date - Dates.Day(days)
q = AirTable(table, AirBase(base))#; filterByFormula="{Status} = 'Todo'")
data = [getfield(i, 3) for i in Airtable.query(q)]
# https://discourse.julialang.org/t/construct-dataframe-from-uneven-named-tuples/102970
cols = union(keys.(data)...)
df = DataFrame([c => get.(data, c, missing) for c in cols]...)
df.Created = Date.(chop.(df.Created, tail=14), "yyyy-mm-dd")
function is_between(date::Dates.Date, start_date::Dates.Date=start_date, end_date::Dates.Date=end_date)::Bool
return (start_date <= date <= end_date)
end
subset!(df, :Created => x -> is_between.(x), skipmissing=true)
sort!(df)
return df
end
"""
airtable_sales_for_period(date::String)
Takes a date in string form yy-mm-dd, tables default to variable list above, number of days defaults to f, base defaults to pack builders.
Returns a returns a pretty table of last 7 days sales.
Note sales recorded in jotform all default to 2023-08-18, the date they were imported into airtable.
"""
function airtable_sales_for_period(date::String, tables=tables::Vector{@NamedTuple{key::String, name::String}}, days::Int=7, base::String="appt7SR7DsTsh22zO")
end_date = Date("$date", dateformat"y-m-d")
if end_date == Dates.today()
end_date -= Dates.Day(1)
end
for t in tables
df = table_sales_for_period(t.key, end_date)
select!(df, :Number, :Created, :Name)
!isempty(df) && println(pretty_table(df; title="$(t.name)", header=["Order Number", "Date", "Name"], crop=:none), "\n") #, display_size = (30, 300)
end
return
end
# /admin/orders.json?created_at_min=2012-08-01 12:00&limit=2012-08-01 12:00
function shopify_sales_for_period(end_date::Dates.Date, days::Int=7)::DataFrame
start_date = end_date - Dates.Day(days)
end
### JOTFORM
#=
# Needs ENV["Jotform"] = "" in startup.jl
# Query api
k=FiordlandPacks.query_jotform("https://api.jotform.com/user/submissions")
# save json to file
using JSON3
open("my2.json", "w") do f
JSON3.pretty(f, JSON3.write(k))
println(f)
end
# ExpandNestedData.jl -> works badly on this json, crap api.
function query_jotform(url::String)
tail = "?apiKey=$(ENV["Jotform"])"
data = JSON3.read(HTTP.get((url*tail)).body)
return data.content
end
using CSV, DataFrames, Dates, DataFramesMeta
# example file: "Downloads/25l_Strong_Pack_Builder2023-08-16_08_54_03.csv"
# Note: mutates the input file, just easier.
function jotform_csv_to_airtable!(file::String)
df=DataFrame(CSV.File(file))
@transform!(df, @byrow :Date = parse(Date, :"Submission Date", Dates.DateFormat("u d, Y")))
f(x) = ismissing(x) ? "" : strip(x) * "\n"
g(x) = ismissing(x) ? "" : string(x)
@transform!(df, @byrow :Address = (f(:"Street Address") * f(:"Street Address Line 2") * f(:"City") * f(:"State / Province") * g(:"Postal / Zip Code")))
@transform!(df, @byrow :Name = titlecase(strip(:"First Name") * " " * strip(:"Last Name")))
select!(df, :Date, :Name, :Address, :)
select!(df, Not([:"Submission Date", :"Street Address", :"Street Address Line 2", :"City", :"State / Province", :"Postal / Zip Code", :"First Name", :"Last Name"]))
rename!(df, :"Side Panels (assuming you have the pack on)" => "Side Panels", :"Side Pockets (assuming you have the pack on)" => "Side Pockets", "Removable Strap Bottle Pockets (+\$25 ea)" => "Removable Strap Bottle Pockets", "Waist Belt Colour and Fabric" => "Waist Belt Fabric and Colour")
#CSV.write(file, df)
return df
end
function validate_exported_df(df::DataFrame, file::String)
std = DataFrame(CSV.File("/Users/david/julia/FiordlandPacks/forms/Pack_Builder_Options.csv"))
#println(setdiff(names(df), names(std)), " ", file) #to check col name differences
#println(length(names(df)), " ", names(df))
#println(names(df)[7:end-1]) #for 25l it is [4:end-1]
@info file
for col in names(df)[7:end-1]
@info col
@info setdiff(df[!, Symbol(col)], std[!, Symbol(col)])
end
println()
end
for i in ["Downloads/65l_Pack_Builder.csv", "Downloads/55l_Pack_Builder.csv", "Downloads/45l_Pack_Builder.csv", "Downloads/35l_Pack_Builder.csv", "Downloads/25l_Strong_Pack_Builder.csv"]
df= jotform_csv_to_airtable!(i) #|> x -> validate_exported_df(x, i)
CSV.write("$(i)_final-2023-08-18", df)
end
=#
end # module