Skip to content

Commit 926ec96

Browse files
committed
Improve subscription product customer management
1 parent 3f50ab9 commit 926ec96

File tree

4 files changed

+221
-33
lines changed

4 files changed

+221
-33
lines changed

clients/apps/web/src/components/Metrics/MiniMetricChartBox.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import {
2+
formatHumanFriendlyCurrency,
3+
formatHumanFriendlyScalar,
4+
} from '@/utils/formatters'
15
import { schemas } from '@polar-sh/client'
26
import {
37
Card,
48
CardContent,
59
CardHeader,
610
} from '@polar-sh/ui/components/atoms/Card'
7-
import { formatCurrencyAndAmount } from '@polar-sh/ui/lib/money'
811

912
export interface MiniMetricBoxProps {
1013
title?: string
@@ -28,10 +31,8 @@ export const MiniMetricChartBox = ({
2831
<h3 className="text-2xl">
2932
{metric &&
3033
(metric.type === 'scalar'
31-
? Intl.NumberFormat('en-US', {
32-
notation: 'compact',
33-
}).format(value ?? 0)
34-
: formatCurrencyAndAmount(value ?? 0, 'USD', 0))}
34+
? formatHumanFriendlyScalar(value ?? 0)
35+
: formatHumanFriendlyCurrency(value ?? 0))}
3536
</h3>
3637
</CardContent>
3738
</Card>

clients/apps/web/src/components/Products/ProductPage/ProductMetricsView.tsx

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,71 @@ export interface ProductMetricsViewProps {
66
data?: ParsedMetricsResponse
77
interval: schemas['TimeInterval']
88
loading: boolean
9+
product: schemas['Product']
910
}
1011

1112
export const ProductMetricsView = ({
1213
data,
1314
loading,
1415
interval,
16+
product,
1517
}: ProductMetricsViewProps) => {
18+
const subscriptionMetrics: (keyof schemas['Metrics'])[] = [
19+
'monthly_recurring_revenue',
20+
'committed_monthly_recurring_revenue',
21+
'active_subscriptions',
22+
'new_subscriptions',
23+
'renewed_subscriptions',
24+
'new_subscriptions_revenue',
25+
'renewed_subscriptions_revenue',
26+
]
27+
28+
const oneTimeMetrics: (keyof schemas['Metrics'])[] = [
29+
'one_time_products',
30+
'one_time_products_revenue',
31+
]
32+
33+
const orderMetrics: (keyof schemas['Metrics'])[] = product.is_recurring
34+
? ['revenue', 'orders', 'average_order_value', 'cumulative_revenue']
35+
: ['average_order_value', 'cumulative_revenue']
36+
1637
return (
1738
<div className="flex flex-col gap-y-12">
18-
<MetricChartBox
19-
data={data}
20-
loading={loading}
21-
metric="orders"
22-
interval={interval}
23-
/>
24-
<MetricChartBox
25-
data={data}
26-
loading={loading}
27-
metric="revenue"
28-
interval={interval}
29-
/>
39+
{product.is_recurring ? (
40+
<>
41+
{subscriptionMetrics.map((metric) => (
42+
<MetricChartBox
43+
key={metric}
44+
data={data}
45+
loading={loading}
46+
metric={metric}
47+
interval={interval}
48+
/>
49+
))}
50+
</>
51+
) : (
52+
<>
53+
{oneTimeMetrics.map((metric) => (
54+
<MetricChartBox
55+
key={metric}
56+
data={data}
57+
loading={loading}
58+
metric={metric}
59+
interval={interval}
60+
/>
61+
))}
62+
</>
63+
)}
64+
65+
{orderMetrics.map((metric) => (
66+
<MetricChartBox
67+
key={metric}
68+
data={data}
69+
loading={loading}
70+
metric={metric}
71+
interval={interval}
72+
/>
73+
))}
3074
</div>
3175
)
3276
}

clients/apps/web/src/components/Products/ProductPage/ProductOverview.tsx

Lines changed: 158 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { MiniMetricChartBox } from '@/components/Metrics/MiniMetricChartBox'
22
import { OrderStatus } from '@/components/Orders/OrderStatus'
3+
import { SubscriptionStatus as SubscriptionStatusComponent } from '@/components/Subscriptions/SubscriptionStatus'
34
import RevenueWidget from '@/components/Widgets/RevenueWidget'
45
import { useDiscounts } from '@/hooks/queries'
56
import { useOrders } from '@/hooks/queries/orders'
7+
import { useSubscriptions } from '@/hooks/queries/subscriptions'
68
import { getDiscountDisplay } from '@/utils/discount'
79
import { schemas } from '@polar-sh/client'
810
import Avatar from '@polar-sh/ui/components/atoms/Avatar'
@@ -36,6 +38,18 @@ export const ProductOverview = ({
3638
},
3739
)
3840

41+
const { data: subscriptions, isLoading: subscriptionsIsLoading } =
42+
useSubscriptions(
43+
product.is_recurring ? organization.id : undefined,
44+
product.is_recurring
45+
? {
46+
product_id: product.id,
47+
active: true,
48+
limit: 10,
49+
}
50+
: undefined,
51+
)
52+
3953
const { data: discountsData, isLoading: discountsLoading } = useDiscounts(
4054
organization.id,
4155
{
@@ -52,29 +66,157 @@ export const ProductOverview = ({
5266
return (
5367
<div className="flex flex-col gap-y-16">
5468
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
55-
<MiniMetricChartBox
56-
metric={metrics?.metrics.orders}
57-
value={metrics?.periods.reduce(
58-
(acc, current) => acc + current.orders,
59-
0,
60-
)}
61-
/>
62-
<MiniMetricChartBox
63-
title="Today's Revenue"
64-
metric={todayMetrics?.metrics.revenue}
65-
value={todayMetrics?.periods[todayMetrics.periods.length - 1].revenue}
66-
/>
69+
{product.is_recurring ? (
70+
<>
71+
<MiniMetricChartBox
72+
title="Active Subscriptions"
73+
metric={metrics?.metrics.active_subscriptions}
74+
value={metrics?.totals.active_subscriptions}
75+
/>
76+
<MiniMetricChartBox
77+
title="Monthly Recurring Revenue"
78+
metric={metrics?.metrics.monthly_recurring_revenue}
79+
value={metrics?.totals.monthly_recurring_revenue}
80+
/>
81+
</>
82+
) : (
83+
<>
84+
<MiniMetricChartBox
85+
metric={metrics?.metrics.orders}
86+
value={metrics?.totals.orders}
87+
/>
88+
<MiniMetricChartBox
89+
title="Today's Revenue"
90+
metric={todayMetrics?.metrics.revenue}
91+
value={todayMetrics?.periods.at(-1)?.revenue}
92+
/>
93+
</>
94+
)}
6795
<MiniMetricChartBox
6896
metric={metrics?.metrics.cumulative_revenue}
69-
value={
70-
metrics?.periods[metrics?.periods.length - 1].cumulative_revenue
71-
}
97+
value={metrics?.periods.at(-1)?.cumulative_revenue}
7298
/>
7399
</div>
100+
{product.is_recurring && (
101+
<div className="flex flex-col gap-y-6">
102+
<div className="flex flex-row items-center justify-between gap-x-6">
103+
<div className="flex flex-col gap-y-1">
104+
<h2 className="text-lg">Subscriptions</h2>
105+
<p className="dark:text-polar-500 text-sm text-gray-500">
106+
Showing 10 most recent subscriptions for {product.name}
107+
</p>
108+
</div>
109+
<Link
110+
href={`/dashboard/${organization.slug}/sales/subscriptions?product_id=${product.id}`}
111+
>
112+
<Button size="sm">View All</Button>
113+
</Link>
114+
</div>
115+
<DataTable
116+
data={subscriptions?.items ?? []}
117+
columns={[
118+
{
119+
id: 'customer',
120+
accessorKey: 'customer',
121+
enableSorting: true,
122+
header: ({ column }) => (
123+
<DataTableColumnHeader column={column} title="Customer" />
124+
),
125+
cell: ({ row: { original: subscription } }) => {
126+
const customer = subscription.customer
127+
return (
128+
<div className="flex flex-row items-center gap-2">
129+
<Avatar
130+
avatar_url={customer.avatar_url}
131+
name={customer.name || customer.email}
132+
/>
133+
<div className="fw-medium overflow-hidden text-ellipsis">
134+
{customer.email}
135+
</div>
136+
</div>
137+
)
138+
},
139+
},
140+
{
141+
accessorKey: 'status',
142+
enableSorting: true,
143+
header: ({ column }) => (
144+
<DataTableColumnHeader column={column} title="Status" />
145+
),
146+
cell: ({ row: { original: subscription } }) => {
147+
return (
148+
<SubscriptionStatusComponent subscription={subscription} />
149+
)
150+
},
151+
},
152+
{
153+
accessorKey: 'started_at',
154+
enableSorting: true,
155+
header: ({ column }) => (
156+
<DataTableColumnHeader
157+
column={column}
158+
title="Subscription Date"
159+
/>
160+
),
161+
cell: (props) => (
162+
<FormattedDateTime datetime={props.getValue() as string} />
163+
),
164+
},
165+
{
166+
accessorKey: 'current_period_end',
167+
enableSorting: true,
168+
header: ({ column }) => (
169+
<DataTableColumnHeader column={column} title="Renewal Date" />
170+
),
171+
cell: ({
172+
getValue,
173+
row: {
174+
original: { status, cancel_at_period_end },
175+
},
176+
}) => {
177+
const datetime = getValue() as string | null
178+
const willRenew =
179+
(status === 'active' || status === 'trialing') &&
180+
!cancel_at_period_end
181+
return datetime && willRenew ? (
182+
<FormattedDateTime datetime={datetime} />
183+
) : (
184+
'—'
185+
)
186+
},
187+
},
188+
{
189+
accessorKey: 'actions',
190+
enableSorting: false,
191+
header: () => null,
192+
cell: (props) => (
193+
<span className="flex flex-row justify-end gap-x-2">
194+
<Link
195+
href={`/dashboard/${organization.slug}/customers?customerId=${props.row.original.customer.id}`}
196+
>
197+
<Button variant="secondary" size="sm">
198+
View Customer
199+
</Button>
200+
</Link>
201+
<Link
202+
href={`/dashboard/${organization.slug}/sales/subscriptions/${props.row.original.id}`}
203+
>
204+
<Button variant="secondary" size="sm">
205+
View Subscription
206+
</Button>
207+
</Link>
208+
</span>
209+
),
210+
},
211+
]}
212+
isLoading={subscriptionsIsLoading}
213+
/>
214+
</div>
215+
)}
74216
<div className="flex flex-col gap-y-6">
75217
<div className="flex flex-row items-center justify-between gap-x-6">
76218
<div className="flex flex-col gap-y-1">
77-
<h2 className="text-lg">Product Orders</h2>
219+
<h2 className="text-lg">Orders</h2>
78220
<p className="dark:text-polar-500 text-sm text-gray-500">
79221
Showing last 10 orders for {product.name}
80222
</p>

clients/apps/web/src/components/Products/ProductPage/ProductPage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ export const ProductPage = ({ organization, product }: ProductPageProps) => {
220220
data={metrics}
221221
interval={allTimeInterval}
222222
loading={metricsLoading}
223+
product={product}
223224
/>
224225
</TabsContent>
225226
<ConfirmModal

0 commit comments

Comments
 (0)