@withpotter/pages-admin 0.13.0-next.114 → 0.13.0-next.115
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.
|
@@ -7,8 +7,9 @@ function priceList(prices, currencies) {
|
|
|
7
7
|
function planLines(plans, currencies, breakdown) {
|
|
8
8
|
const earned = /* @__PURE__ */ new Map();
|
|
9
9
|
for (const row of breakdown) {
|
|
10
|
-
const
|
|
11
|
-
earned.
|
|
10
|
+
const key = row.planId.toLowerCase();
|
|
11
|
+
const seen = earned.get(key);
|
|
12
|
+
earned.set(key, {
|
|
12
13
|
subscribers: (seen?.subscribers ?? 0) + row.subscribers,
|
|
13
14
|
mrr: (seen?.mrr ?? 0) + row.mrr
|
|
14
15
|
});
|
|
@@ -16,7 +17,7 @@ function planLines(plans, currencies, breakdown) {
|
|
|
16
17
|
return plans.map((plan) => {
|
|
17
18
|
const version = plan.live ?? plan.open;
|
|
18
19
|
const prices = version?.prices ?? {};
|
|
19
|
-
const hit = earned.get(plan.id);
|
|
20
|
+
const hit = earned.get(plan.id.toLowerCase()) ?? earned.get(plan.slug.toLowerCase()) ?? (version ? earned.get(version.name.toLowerCase()) : void 0);
|
|
20
21
|
return {
|
|
21
22
|
id: plan.id,
|
|
22
23
|
name: version?.name ?? plan.slug,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/plan-lines.ts"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport type { PriceCurrency } from '@withpotter/lib/admin-pricing-api';\nimport { byCurrency, formatPlanPrice } from '@withpotter/lib/plan-price';\nimport type { PlanSummary } from '@withpotter/lib/pricing-plans-types';\n\n/** One line per currency, at that currency's exponent. */\nfunction priceList(prices: Record<string, string>, currencies: PriceCurrency[]): string[] {\n return Object.keys(prices)\n .sort(byCurrency)\n .map((code) =>\n formatPlanPrice(prices[code], code, currencies.find((c) => c.code === code)?.exponent),\n );\n}\n\n/** What one plan shows: its catalogue entry joined with what it earns. */\nexport interface PlanLine {\n id: string;\n name: string;\n slug: string;\n prices: string[];\n /** Nothing to pay: every price is zero. */\n free: boolean;\n /** Created but not live yet. */\n draft: boolean;\n subscribers: number;\n mrr: number;\n}\n\n/**\n * Every plan an admin has created, one line each, with the name it was given\n * (Free, Basic, Premium and so on). The plans list is the source of the rows, so\n * a plan nobody is on yet still shows, with 0 subscribers; the revenue figures\n * for it are joined in by plan id.\n */\nexport function planLines(\n plans: readonly PlanSummary[],\n currencies: PriceCurrency[],\n breakdown: PlatformRevenue['recurring']['planBreakdown'],\n): PlanLine[] {\n const earned = new Map<string, { subscribers: number; mrr: number }>();\n\n for (const row of breakdown) {\n const seen = earned.get(
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/plan-lines.ts"],"sourcesContent":["import type { PlatformRevenue } from '@withpotter/lib/admin-api';\nimport type { PriceCurrency } from '@withpotter/lib/admin-pricing-api';\nimport { byCurrency, formatPlanPrice } from '@withpotter/lib/plan-price';\nimport type { PlanSummary } from '@withpotter/lib/pricing-plans-types';\n\n/** One line per currency, at that currency's exponent. */\nfunction priceList(prices: Record<string, string>, currencies: PriceCurrency[]): string[] {\n return Object.keys(prices)\n .sort(byCurrency)\n .map((code) =>\n formatPlanPrice(prices[code], code, currencies.find((c) => c.code === code)?.exponent),\n );\n}\n\n/** What one plan shows: its catalogue entry joined with what it earns. */\nexport interface PlanLine {\n id: string;\n name: string;\n slug: string;\n prices: string[];\n /** Nothing to pay: every price is zero. */\n free: boolean;\n /** Created but not live yet. */\n draft: boolean;\n subscribers: number;\n mrr: number;\n}\n\n/**\n * Every plan an admin has created, one line each, with the name it was given\n * (Free, Basic, Premium and so on). The plans list is the source of the rows, so\n * a plan nobody is on yet still shows, with 0 subscribers; the revenue figures\n * for it are joined in by plan id, code or name.\n */\nexport function planLines(\n plans: readonly PlanSummary[],\n currencies: PriceCurrency[],\n breakdown: PlatformRevenue['recurring']['planBreakdown'],\n): PlanLine[] {\n const earned = new Map<string, { subscribers: number; mrr: number }>();\n\n for (const row of breakdown) {\n const key = row.planId.toLowerCase();\n const seen = earned.get(key);\n\n // A plan billed monthly and yearly comes as two rows: add them up.\n earned.set(key, {\n subscribers: (seen?.subscribers ?? 0) + row.subscribers,\n mrr: (seen?.mrr ?? 0) + row.mrr,\n });\n }\n\n return plans\n .map((plan): PlanLine => {\n const version = plan.live ?? plan.open;\n const prices: Record<string, string> = version?.prices ?? {};\n // The revenue breakdown names a plan by its code (\"basic\"), the plans list by its id: match either, or the name.\n const hit =\n earned.get(plan.id.toLowerCase()) ??\n earned.get(plan.slug.toLowerCase()) ??\n (version ? earned.get(version.name.toLowerCase()) : undefined);\n\n return {\n id: plan.id,\n name: version?.name ?? plan.slug,\n slug: plan.slug,\n prices: version ? priceList(prices, currencies) : [],\n free:\n Object.keys(prices).length > 0 &&\n Object.values(prices).every((minor) => /^0*$/.test(minor)),\n draft: plan.live === null,\n subscribers: hit?.subscribers ?? plan.subscribers,\n mrr: hit?.mrr ?? 0,\n };\n })\n .sort((a, b) => b.mrr - a.mrr || b.subscribers - a.subscribers || a.name.localeCompare(b.name));\n}\n"],"mappings":"AAEA,SAAS,YAAY,uBAAuB;AAI5C,SAAS,UAAU,QAAgC,YAAuC;AACxF,SAAO,OAAO,KAAK,MAAM,EACtB,KAAK,UAAU,EACf;AAAA,IAAI,CAAC,SACJ,gBAAgB,OAAO,IAAI,GAAG,MAAM,WAAW,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,GAAG,QAAQ;AAAA,EACvF;AACJ;AAsBO,SAAS,UACd,OACA,YACA,WACY;AACZ,QAAM,SAAS,oBAAI,IAAkD;AAErE,aAAW,OAAO,WAAW;AAC3B,UAAM,MAAM,IAAI,OAAO,YAAY;AACnC,UAAM,OAAO,OAAO,IAAI,GAAG;AAG3B,WAAO,IAAI,KAAK;AAAA,MACd,cAAc,MAAM,eAAe,KAAK,IAAI;AAAA,MAC5C,MAAM,MAAM,OAAO,KAAK,IAAI;AAAA,IAC9B,CAAC;AAAA,EACH;AAEA,SAAO,MACJ,IAAI,CAAC,SAAmB;AACvB,UAAM,UAAU,KAAK,QAAQ,KAAK;AAClC,UAAM,SAAiC,SAAS,UAAU,CAAC;AAE3D,UAAM,MACJ,OAAO,IAAI,KAAK,GAAG,YAAY,CAAC,KAChC,OAAO,IAAI,KAAK,KAAK,YAAY,CAAC,MACjC,UAAU,OAAO,IAAI,QAAQ,KAAK,YAAY,CAAC,IAAI;AAEtD,WAAO;AAAA,MACL,IAAI,KAAK;AAAA,MACT,MAAM,SAAS,QAAQ,KAAK;AAAA,MAC5B,MAAM,KAAK;AAAA,MACX,QAAQ,UAAU,UAAU,QAAQ,UAAU,IAAI,CAAC;AAAA,MACnD,MACE,OAAO,KAAK,MAAM,EAAE,SAAS,KAC7B,OAAO,OAAO,MAAM,EAAE,MAAM,CAAC,UAAU,OAAO,KAAK,KAAK,CAAC;AAAA,MAC3D,OAAO,KAAK,SAAS;AAAA,MACrB,aAAa,KAAK,eAAe,KAAK;AAAA,MACtC,KAAK,KAAK,OAAO;AAAA,IACnB;AAAA,EACF,CAAC,EACA,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAClG;","names":[]}
|
|
@@ -89,8 +89,8 @@ function RevenueOverview({
|
|
|
89
89
|
}
|
|
90
90
|
) }) }),
|
|
91
91
|
recurringTracked && /* @__PURE__ */ jsxs("div", { className: "adm-grid", children: [
|
|
92
|
-
/* @__PURE__ */ jsx("div", { className: "adm-span-7", children: /* @__PURE__ */ jsx(HealthPanel, { health, money, caption: recurringCaption }) }),
|
|
93
|
-
/* @__PURE__ */ jsx("div", { className: "adm-span-5", children: plans ? /* @__PURE__ */ jsx(
|
|
92
|
+
/* @__PURE__ */ jsx("div", { className: plans ? "adm-span-12" : "adm-span-7", children: /* @__PURE__ */ jsx(HealthPanel, { health, money, caption: recurringCaption }) }),
|
|
93
|
+
/* @__PURE__ */ jsx("div", { className: plans ? "adm-span-12" : "adm-span-5", children: plans ? /* @__PURE__ */ jsx(
|
|
94
94
|
PlansPanel,
|
|
95
95
|
{
|
|
96
96
|
lines: planLines(plans.plans, plans.currencies, data.recurring.planBreakdown),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/revenue/revenue-overview.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { TimeSeriesChart } from '@withpotter/components/charts';\nimport type {\n GatewayChannelTotal,\n MerchantPaymentsPage,\n PlatformCockpit,\n PlatformRevenue,\n} from '@withpotter/lib/admin-api';\nimport type { PlanList } from '@withpotter/lib/pricing-plans-types';\nimport {\n MoneyLines,\n PICK_CURRENCY,\n PageTabs,\n Panel,\n StatCard,\n moneyIn,\n rowFor,\n toTrend,\n useTabParam,\n} from '../_ui';\nimport { Reveal } from '../_fx';\nimport { Tinted } from '../audit/tinted';\nimport { ChannelPanel } from './channel-panel';\nimport { MerchantHeadline } from './merchant-headline';\nimport { MethodCards } from './method-cards';\nimport { HealthPanel } from './health-panel';\nimport { PlanMixPanel } from './plan-mix-panel';\nimport { planLines } from './plan-lines';\nimport { PlansPanel } from './plans-panel';\nimport { platformFeeRows } from './platform-fees';\nimport { RevenueKpis } from './revenue-kpis';\nimport { SubscriptionsPanel } from './subscriptions-panel';\nimport { TopTenantsPanel } from './top-tenants-panel';\nimport styles from './revenue.module.css';\nimport ledgerStyles from './ledger.module.css';\n\n/** The Overview's own tabs, in the order the money is read: Olive's, how customers pay, merchants', then plans. */\nconst VIEWS = [\n { id: 'olive', label: 'Money Olive earns' },\n { id: 'customers', label: 'How customers pay' },\n { id: 'merchants', label: 'Money merchants earn' },\n { id: 'plans', label: 'Plan subscriptions' },\n] as const;\n\nconst VIEW_KEYS = VIEWS.map((v) => v.id);\n\n/**\n * Overview tab, one view at a time: what Olive earns (headline figures, fees, revenue health and plan mix), how\n * customers pay, what merchants earn (the GMV trend and the leaderboard), and the plan subscriptions. Merchant GMV is\n * per currency: the chart and the leaderboard follow one currency (`focus`), never a mix.\n */\nexport function RevenueOverview({\n data,\n health,\n money,\n money2,\n currency,\n focus,\n period,\n span,\n recurringCaption,\n channels,\n ledger,\n plans,\n}: {\n data: PlatformRevenue;\n health: PlatformCockpit['health'] | undefined;\n /** Formats recurring revenue, in the currency the API names for it. */\n money: (value: number) => string;\n money2: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the chart and leaderboard show, or null. */\n focus: string | null;\n period: string;\n span: string;\n recurringCaption: string;\n /** How customers paid, per channel and currency. Absent when the box cannot say. */\n channels?: readonly GatewayChannelTotal[];\n /** The merchant payments view's totals, with the period before; absent on a box without it. */\n ledger?: MerchantPaymentsPage;\n /** Every plan created, from Pricing. Absent while it loads or when it cannot be read. */\n plans?: PlanList;\n}) {\n const [view, setView] = useTabParam(VIEW_KEYS, 'view');\n const gmv = rowFor(data.gmvByCurrency, focus);\n const gmvSeries = useMemo(() => toTrend(gmv?.series, 'gmv'), [gmv]);\n const gmvCaption = focus ? `${period} · ${focus}` : period;\n const recurringTracked = data.recurring.tracked !== false;\n // Hidden until the api sends the field; never an invented zero.\n const platformFees = platformFeeRows(data.platformFeesEarnedByCurrency, currency);\n\n const olive = (\n <>\n <p className={ledgerStyles.sectionNote}>\n What merchants pay Olive: their plan payments, and the fees Olive keeps.\n {recurringTracked ? '' : ' Plan income is not set up on this box yet.'}\n </p>\n <RevenueKpis\n part=\"olive\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={[]}\n />\n\n {platformFees && (\n <section aria-label=\"Platform fees\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Fees Olive keeps\"\n value={\n <MoneyLines\n rows={platformFees}\n currency=\"\"\n amount={(r) => r.amount}\n decimals={2}\n />\n }\n hint=\"What Olive keeps from payouts and plan payments, after costs.\"\n />\n </Reveal>\n </section>\n )}\n\n {recurringTracked && (\n <div className=\"adm-grid\">\n <div className=\"adm-span-7\">\n <HealthPanel health={health} money={money} caption={recurringCaption} />\n </div>\n <div className=\"adm-span-5\">\n {plans ? (\n <PlansPanel\n lines={planLines(plans.plans, plans.currencies, data.recurring.planBreakdown)}\n money={money}\n caption={recurringCaption}\n />\n ) : (\n <PlanMixPanel\n planBreakdown={data.recurring.planBreakdown}\n money={money}\n caption={recurringCaption}\n />\n )}\n </div>\n </div>\n )}\n </>\n );\n\n const customers = (\n <>\n <p className={ledgerStyles.sectionNote}>\n The ways of paying on the checkout, and how much came in through each.\n </p>\n {ledger && focus ? (\n <MethodCards\n now={ledger.byChannel.current}\n before={ledger.byChannel.previous}\n currency={focus}\n />\n ) : channels && channels.length > 0 ? (\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <ChannelPanel totals={channels} focus={focus} caption={gmvCaption} />\n </div>\n </div>\n ) : (\n <p className={styles.empty}>{focus ? 'Nothing to show for this period.' : PICK_CURRENCY}</p>\n )}\n </>\n );\n\n const merchants = (\n <>\n <p className={ledgerStyles.sectionNote}>\n What customers paid to merchants. This is the merchants' money, not Olive's own income.\n </p>\n <RevenueKpis\n part=\"merchant\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={gmvSeries.map((p) => p.value)}\n extra={ledger && focus ? <MerchantHeadline ledger={ledger} focus={focus} /> : null}\n />\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel\n title=\"Merchant sales over time\"\n caption={gmvCaption}\n height=\"calc(var(--adm-panel-h) + 32px)\"\n >\n {focus ? (\n <div className={styles.flatChart}>\n <Tinted tone=\"money\">\n <TimeSeriesChart\n data={gmvSeries}\n compare={null}\n compareLabel={`Previous ${span}`}\n formatValue={moneyIn(focus)}\n emptyMessage={`No paid ${focus} orders in this window yet`}\n height={240}\n />\n </Tinted>\n </div>\n ) : (\n <p className={styles.empty}>{PICK_CURRENCY}</p>\n )}\n </Panel>\n </div>\n </div>\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <TopTenantsPanel\n tenants={gmv?.topTenants ?? []}\n lifetime={gmv?.lifetime ?? 0}\n focus={focus}\n />\n </div>\n </div>\n </>\n );\n\n const planSubscriptions = recurringTracked ? (\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <SubscriptionsPanel recurring={data.recurring} span={span} />\n </div>\n </div>\n ) : (\n <p className={styles.empty}>Plan income is not set up on this box yet.</p>\n );\n\n return (\n <>\n <PageTabs tabs={VIEWS} active={view} onChange={setView} />\n {view === 'olive' && olive}\n {view === 'customers' && customers}\n {view === 'merchants' && merchants}\n {view === 'plans' && planSubscriptions}\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/revenue/revenue-overview.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { TimeSeriesChart } from '@withpotter/components/charts';\nimport type {\n GatewayChannelTotal,\n MerchantPaymentsPage,\n PlatformCockpit,\n PlatformRevenue,\n} from '@withpotter/lib/admin-api';\nimport type { PlanList } from '@withpotter/lib/pricing-plans-types';\nimport {\n MoneyLines,\n PICK_CURRENCY,\n PageTabs,\n Panel,\n StatCard,\n moneyIn,\n rowFor,\n toTrend,\n useTabParam,\n} from '../_ui';\nimport { Reveal } from '../_fx';\nimport { Tinted } from '../audit/tinted';\nimport { ChannelPanel } from './channel-panel';\nimport { MerchantHeadline } from './merchant-headline';\nimport { MethodCards } from './method-cards';\nimport { HealthPanel } from './health-panel';\nimport { PlanMixPanel } from './plan-mix-panel';\nimport { planLines } from './plan-lines';\nimport { PlansPanel } from './plans-panel';\nimport { platformFeeRows } from './platform-fees';\nimport { RevenueKpis } from './revenue-kpis';\nimport { SubscriptionsPanel } from './subscriptions-panel';\nimport { TopTenantsPanel } from './top-tenants-panel';\nimport styles from './revenue.module.css';\nimport ledgerStyles from './ledger.module.css';\n\n/** The Overview's own tabs, in the order the money is read: Olive's, how customers pay, merchants', then plans. */\nconst VIEWS = [\n { id: 'olive', label: 'Money Olive earns' },\n { id: 'customers', label: 'How customers pay' },\n { id: 'merchants', label: 'Money merchants earn' },\n { id: 'plans', label: 'Plan subscriptions' },\n] as const;\n\nconst VIEW_KEYS = VIEWS.map((v) => v.id);\n\n/**\n * Overview tab, one view at a time: what Olive earns (headline figures, fees, revenue health and plan mix), how\n * customers pay, what merchants earn (the GMV trend and the leaderboard), and the plan subscriptions. Merchant GMV is\n * per currency: the chart and the leaderboard follow one currency (`focus`), never a mix.\n */\nexport function RevenueOverview({\n data,\n health,\n money,\n money2,\n currency,\n focus,\n period,\n span,\n recurringCaption,\n channels,\n ledger,\n plans,\n}: {\n data: PlatformRevenue;\n health: PlatformCockpit['health'] | undefined;\n /** Formats recurring revenue, in the currency the API names for it. */\n money: (value: number) => string;\n money2: (value: number) => string;\n /** The currency filter's choice; empty shows a line per currency. */\n currency: string;\n /** The one currency the chart and leaderboard show, or null. */\n focus: string | null;\n period: string;\n span: string;\n recurringCaption: string;\n /** How customers paid, per channel and currency. Absent when the box cannot say. */\n channels?: readonly GatewayChannelTotal[];\n /** The merchant payments view's totals, with the period before; absent on a box without it. */\n ledger?: MerchantPaymentsPage;\n /** Every plan created, from Pricing. Absent while it loads or when it cannot be read. */\n plans?: PlanList;\n}) {\n const [view, setView] = useTabParam(VIEW_KEYS, 'view');\n const gmv = rowFor(data.gmvByCurrency, focus);\n const gmvSeries = useMemo(() => toTrend(gmv?.series, 'gmv'), [gmv]);\n const gmvCaption = focus ? `${period} · ${focus}` : period;\n const recurringTracked = data.recurring.tracked !== false;\n // Hidden until the api sends the field; never an invented zero.\n const platformFees = platformFeeRows(data.platformFeesEarnedByCurrency, currency);\n\n const olive = (\n <>\n <p className={ledgerStyles.sectionNote}>\n What merchants pay Olive: their plan payments, and the fees Olive keeps.\n {recurringTracked ? '' : ' Plan income is not set up on this box yet.'}\n </p>\n <RevenueKpis\n part=\"olive\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={[]}\n />\n\n {platformFees && (\n <section aria-label=\"Platform fees\" className=\"adm-grid\">\n <Reveal index={0} className=\"adm-span-3\">\n <StatCard\n label=\"Fees Olive keeps\"\n value={\n <MoneyLines\n rows={platformFees}\n currency=\"\"\n amount={(r) => r.amount}\n decimals={2}\n />\n }\n hint=\"What Olive keeps from payouts and plan payments, after costs.\"\n />\n </Reveal>\n </section>\n )}\n\n {recurringTracked && (\n <div className=\"adm-grid\">\n <div className={plans ? 'adm-span-12' : 'adm-span-7'}>\n <HealthPanel health={health} money={money} caption={recurringCaption} />\n </div>\n {/* The Plans table has five columns, so it gets a full row; the older plan mix keeps its narrow slot. */}\n <div className={plans ? 'adm-span-12' : 'adm-span-5'}>\n {plans ? (\n <PlansPanel\n lines={planLines(plans.plans, plans.currencies, data.recurring.planBreakdown)}\n money={money}\n caption={recurringCaption}\n />\n ) : (\n <PlanMixPanel\n planBreakdown={data.recurring.planBreakdown}\n money={money}\n caption={recurringCaption}\n />\n )}\n </div>\n </div>\n )}\n </>\n );\n\n const customers = (\n <>\n <p className={ledgerStyles.sectionNote}>\n The ways of paying on the checkout, and how much came in through each.\n </p>\n {ledger && focus ? (\n <MethodCards\n now={ledger.byChannel.current}\n before={ledger.byChannel.previous}\n currency={focus}\n />\n ) : channels && channels.length > 0 ? (\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <ChannelPanel totals={channels} focus={focus} caption={gmvCaption} />\n </div>\n </div>\n ) : (\n <p className={styles.empty}>{focus ? 'Nothing to show for this period.' : PICK_CURRENCY}</p>\n )}\n </>\n );\n\n const merchants = (\n <>\n <p className={ledgerStyles.sectionNote}>\n What customers paid to merchants. This is the merchants' money, not Olive's own income.\n </p>\n <RevenueKpis\n part=\"merchant\"\n data={data}\n money={money}\n money2={money2}\n currency={currency}\n focus={focus}\n gmvSpark={gmvSeries.map((p) => p.value)}\n extra={ledger && focus ? <MerchantHeadline ledger={ledger} focus={focus} /> : null}\n />\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <Panel\n title=\"Merchant sales over time\"\n caption={gmvCaption}\n height=\"calc(var(--adm-panel-h) + 32px)\"\n >\n {focus ? (\n <div className={styles.flatChart}>\n <Tinted tone=\"money\">\n <TimeSeriesChart\n data={gmvSeries}\n compare={null}\n compareLabel={`Previous ${span}`}\n formatValue={moneyIn(focus)}\n emptyMessage={`No paid ${focus} orders in this window yet`}\n height={240}\n />\n </Tinted>\n </div>\n ) : (\n <p className={styles.empty}>{PICK_CURRENCY}</p>\n )}\n </Panel>\n </div>\n </div>\n\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <TopTenantsPanel\n tenants={gmv?.topTenants ?? []}\n lifetime={gmv?.lifetime ?? 0}\n focus={focus}\n />\n </div>\n </div>\n </>\n );\n\n const planSubscriptions = recurringTracked ? (\n <div className=\"adm-grid\">\n <div className=\"adm-span-12\">\n <SubscriptionsPanel recurring={data.recurring} span={span} />\n </div>\n </div>\n ) : (\n <p className={styles.empty}>Plan income is not set up on this box yet.</p>\n );\n\n return (\n <>\n <PageTabs tabs={VIEWS} active={view} onChange={setView} />\n {view === 'olive' && olive}\n {view === 'customers' && customers}\n {view === 'merchants' && merchants}\n {view === 'plans' && planSubscriptions}\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withpotter/pages-admin",
|
|
3
|
-
"version": "0.13.0-next.
|
|
3
|
+
"version": "0.13.0-next.115",
|
|
4
4
|
"description": "Box console platform-admin routes: overview, revenue, accounts, tenants, admins, roles, audit.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@radix-ui/react-popover": "^1.1.23",
|
|
33
33
|
"@tanstack/react-query": "^5.90.20",
|
|
34
|
-
"@withpotter/lib": "^0.13.0-next.
|
|
35
|
-
"@withpotter/platform-admin": "^0.13.0-next.
|
|
36
|
-
"@withpotter/ui-kit": "^0.13.0-next.
|
|
34
|
+
"@withpotter/lib": "^0.13.0-next.115",
|
|
35
|
+
"@withpotter/platform-admin": "^0.13.0-next.115",
|
|
36
|
+
"@withpotter/ui-kit": "^0.13.0-next.115",
|
|
37
37
|
"lucide-react": "^0.562.0",
|
|
38
38
|
"motion": "^12.42.2"
|
|
39
39
|
},
|