import type { HeadersFunction, LoaderFunctionArgs } from "react-router"; import { Form, useLoaderData } from "react-router"; import { boundary } from "@shopify/shopify-app-react-router/server"; import { authenticate } from "../shopify.server"; import db from "../db.server"; import { ensureShop } from "../lib/shop.server"; import { planOf } from "../lib/plans"; import { moneyFormatter } from "../lib/offer-text"; import { formatShopDate } from "../lib/tz"; import { decrypt } from "../lib/crypto.server"; const RANGES: Record = { "7": 7, "30": 30, "90": 90, "365": 365 }; export const loader = async ({ request }: LoaderFunctionArgs) => { const { admin, session } = await authenticate.admin(request); const shop = await ensureShop(admin, session.shop); const url = new URL(request.url); const days = RANGES[url.searchParams.get("days") || "30"] || 30; const offerFilter = url.searchParams.get("offer") || ""; const since = new Date(Date.now() - days * 86400000); const money = moneyFormatter(shop.currency); const usages = await db.orderUsage.findMany({ where: { shopId: shop.id, createdAt: { gte: since } }, orderBy: { createdAt: "desc" } }); const stats = await db.orderStat.findMany({ where: { shopId: shop.id, createdAt: { gte: since } } }); const byOffer = new Map; revenue: number; discount: number }>(); for (const u of usages) { const e = byOffer.get(u.offerName) || { name: u.offerName, orders: new Set(), revenue: 0, discount: 0 }; e.orders.add(u.orderId); e.revenue += Number(u.bundleRevenue); e.discount += Number(u.discount); byOffer.set(u.offerName, e); } const avg = (xs: number[]) => (xs.length ? xs.reduce((s, x) => s + x, 0) / xs.length : 0); const withB = stats.filter((s) => s.hasBundle).map((s) => Number(s.total)); const without = stats.filter((s) => !s.hasBundle).map((s) => Number(s.total)); const offers = [...byOffer.values()].sort((a, b) => b.revenue - a.revenue); const recent = usages.filter((u) => !offerFilter || u.offerName === offerFilter).slice(0, 50); return { allowed: planOf(shop.planCode).analytics, days: String(days), offerFilter, totals: { orders: new Set(usages.map((u) => u.orderId)).size, revenue: money(offers.reduce((s, o) => s + o.revenue, 0)), discount: money(offers.reduce((s, o) => s + o.discount, 0)), aovWith: money(avg(withB)), aovWithout: money(avg(without)), lift: avg(without) ? `${Math.round(((avg(withB) - avg(without)) / avg(without)) * 100)}%` : "—", }, offers: offers.map((o) => ({ name: o.name, orders: o.orders.size, revenue: money(o.revenue), discount: money(o.discount) })), recent: recent.map((u) => ({ id: u.id, orderId: decrypt(u.orderRef) || "", orderName: decrypt(u.orderName) || "Order", offer: u.offerName, discount: money(Number(u.discount)), at: formatShopDate(u.createdAt, shop.timezone), })), shopDomain: shop.domain, }; }; export default function Analytics() { const d = useLoaderData(); return ( {!d.allowed ? ( You can still see orders per offer below. See plans ) : null}
Last 7 days Last 30 days Last 90 days Last 12 months {d.offerFilter ? : null} Apply
{d.allowed ? ( {[ ["Bundle orders", String(d.totals.orders)], ["Bundle revenue", d.totals.revenue], ["Discounts given", d.totals.discount], ["AOV with a bundle", d.totals.aovWith], ["AOV without", d.totals.aovWithout], ["AOV lift", d.totals.lift], ].map(([label, value]) => ( {label}{value} ))} ) : null} {d.offers.length ? ( Offer Orders {d.allowed ? Bundle revenue : null} {d.allowed ? Discount : null} {d.offers.map((o) => ( {o.name} {o.orders} {d.allowed ? {o.revenue} : null} {d.allowed ? {o.discount} : null} ))} ) : No bundle orders in this period yet.} {d.recent.length ? ( Order Offer Discount Date {d.recent.map((r) => ( {r.orderId ? {r.orderName} : r.orderName} {r.offer} {r.discount} {r.at} ))} ) : No orders.}
); } export const headers: HeadersFunction = (headersArgs) => boundary.headers(headersArgs);