Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions clients/apps/web/src/components/Customer/CustomerBenefit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
'use client'

import { useCustomerBenefitGrantsList } from '@/hooks/queries/benefits'
import { DataTableSortingState } from '@/utils/datatable'
import { schemas } from '@polar-sh/client'
import Button from '@polar-sh/ui/components/atoms/Button'
import { DataTable } from '@polar-sh/ui/components/atoms/DataTable'
import FormattedDateTime from '@polar-sh/ui/components/atoms/FormattedDateTime'
import { Status } from '@polar-sh/ui/components/atoms/Status'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@polar-sh/ui/components/ui/tooltip'
import Link from 'next/link'
import React from 'react'
import { twMerge } from 'tailwind-merge'

interface CustomerBenefitProps {
customer: schemas['Customer']
organization: schemas['Organization']
}

export const CustomerBenefit: React.FC<CustomerBenefitProps> = ({
customer,
organization,
}) => {
const [sorting, setSorting] = React.useState<DataTableSortingState>([])

const { data: benefitGrants, isLoading } = useCustomerBenefitGrantsList({
customerId: customer.id,
organizationId: organization.id,
})

return (
<div className="flex flex-col gap-6">
<h2 className="text-xl">Customer Benefits</h2>
{!isLoading && benefitGrants?.length === 0 && (
<div className="flex flex-col items-center gap-y-6">
<div className="flex flex-col items-center gap-y-2">
<h3 className="text-lg font-medium">No benefits granted</h3>
<p className="dark:text-polar-500 text-gray-500">
This customer has no benefit grants.
</p>
</div>
</div>
)}
{(isLoading || (benefitGrants && benefitGrants.length > 0)) && (
<DataTable
data={benefitGrants || []}
isLoading={isLoading}
sorting={sorting}
onSortingChange={setSorting}
columns={[
{
accessorKey: 'benefit',
header: 'Benefit',
cell: ({ row: { original: grant } }: { row: { original: schemas['BenefitGrant'] & { benefit: schemas['Benefit'] } } }) => (
<div className="flex items-center gap-3">
<div className="flex min-w-0 flex-col">
<div className="w-full truncate text-sm font-medium">
{grant.benefit.description}
</div>
<div className="w-full truncate text-xs text-gray-500 dark:text-gray-400">
{grant.benefit.type.replace('_', ' ').split(' ').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ')}
</div>
</div>
</div>
),
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row: { original: grant } }: { row: { original: schemas['BenefitGrant'] & { benefit: schemas['Benefit'] } } }) => {
const isRevoked = grant.revoked_at !== null
const isGranted = grant.is_granted
const hasError = grant.error !== null

const status = hasError
? 'Error'
: isRevoked
? 'Revoked'
: isGranted
? 'Granted'
: 'Pending'

const statusDescription = {
Revoked: 'The customer does not have access to this benefit',
Granted: 'The customer has access to this benefit',
Pending: 'The benefit grant is currently being processed',
Error: grant.error?.message ?? 'An unknown error occurred',
}

const statusClassNames = {
Revoked: 'bg-red-100 text-red-500 dark:bg-red-950',
Granted: 'bg-emerald-200 text-emerald-500 dark:bg-emerald-950',
Pending: 'bg-yellow-100 text-yellow-500 dark:bg-yellow-950',
Error: 'bg-red-100 text-red-500 dark:bg-red-950',
}

return (
<Tooltip>
<TooltipTrigger>
<Status
className={twMerge('w-fit', statusClassNames[status])}
status={status}
/>
</TooltipTrigger>
<TooltipContent>{statusDescription[status]}</TooltipContent>
</Tooltip>
)
},
},
{
accessorKey: 'created_at',
header: 'Granted',
cell: ({ row: { original: grant } }: { row: { original: schemas['BenefitGrant'] & { benefit: schemas['Benefit'] } } }) => (
<FormattedDateTime datetime={grant.created_at} />
),
},
{
accessorKey: 'revoked_at',
header: 'Revoked',
cell: ({ row: { original: grant } }: { row: { original: schemas['BenefitGrant'] & { benefit: schemas['Benefit'] } } }) =>
grant.revoked_at ? (
<FormattedDateTime datetime={grant.revoked_at} />
) : (
<span className="text-gray-400">—</span>
),
},
{
accessorKey: 'order',
header: 'Order',
cell: ({ row: { original: grant } }: { row: { original: schemas['BenefitGrant'] & { benefit: schemas['Benefit'] } } }) =>
grant.order_id ? (
<Link
href={`/dashboard/${organization.slug}/sales/${grant.order_id}`}
>
<Button size="sm" variant="secondary">
View Order
</Button>
</Link>
) : (
<span className="text-gray-400">—</span>
),
},
{
accessorKey: 'subscription',
header: 'Subscription',
cell: ({ row: { original: grant } }: { row: { original: schemas['BenefitGrant'] & { benefit: schemas['Benefit'] } } }) =>
grant.subscription_id ? (
<Link
href={`/dashboard/${organization.slug}/sales/subscriptions/${grant.subscription_id}`}
>
<Button size="sm" variant="secondary">
View Subscription
</Button>
</Link>
) : (
<span className="text-gray-400">—</span>
),
},
]}
/>
)}
</div>
)
}
5 changes: 5 additions & 0 deletions clients/apps/web/src/components/Customer/CustomerPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'

import { CustomerBenefit } from '@/components/Customer/CustomerBenefit'
import { CustomerEventsView } from '@/components/Customer/CustomerEventsView'
import { CustomerUsageView } from '@/components/Customer/CustomerUsageView'
import AmountLabel from '@/components/Shared/AmountLabel'
Expand Down Expand Up @@ -68,6 +69,7 @@ export const CustomerPage: React.FC<CustomerPageProps> = ({
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="events">Events</TabsTrigger>
<TabsTrigger value="usage">Usage</TabsTrigger>
<TabsTrigger value="benefits">Benefits</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="flex flex-col gap-y-12">
<MetricChartBox
Expand Down Expand Up @@ -249,6 +251,9 @@ export const CustomerPage: React.FC<CustomerPageProps> = ({
<TabsContent value="events">
<CustomerEventsView customer={customer} organization={organization} />
</TabsContent>
<TabsContent value="benefits">
<CustomerBenefit customer={customer} organization={organization} />
</TabsContent>
</Tabs>
)
}
56 changes: 56 additions & 0 deletions clients/apps/web/src/hooks/queries/benefits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,59 @@ export const useBenefitGrants = ({
},
retry: defaultRetry,
});

export const useCustomerBenefitGrantsList = ({
customerId,
organizationId,
}: {
customerId: string;
organizationId: string;
}) =>
useQuery({
queryKey: [
"customer",
"benefit_grants",
customerId,
organizationId,
],
queryFn: async () => {
const benefitsResponse = await unwrap(
api.GET("/v1/benefits/", {
params: {
query: {
organization_id: organizationId,
limit: 100,
},
},
}),
);

const allGrants: (schemas['BenefitGrant'] & { benefit: schemas['Benefit'] })[] = [];

for (const benefit of benefitsResponse.items) {
const grantsResponse = await unwrap(
api.GET("/v1/benefits/{id}/grants", {
params: {
path: { id: benefit.id },
query: {
customer_id: customerId,
limit: 1000,
},
},
}),
);
const grantsWithBenefit = grantsResponse.items.map(grant => ({
...grant,
benefit,
}));
allGrants.push(...grantsWithBenefit);
}

const sortedGrants = allGrants.sort((a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);

return sortedGrants;
},
retry: defaultRetry,
});