From b847c9703d27b44410f77775f48d521af4a995ab Mon Sep 17 00:00:00 2001 From: Manas Kenge Date: Sun, 14 Sep 2025 10:32:37 +0530 Subject: [PATCH 1/2] WIP --- .../components/Customer/CustomerBenefit.tsx | 178 ++++++++++++++++++ .../src/components/Customer/CustomerPage.tsx | 5 + .../apps/web/src/hooks/queries/benefits.ts | 76 ++++++++ 3 files changed, 259 insertions(+) create mode 100644 clients/apps/web/src/components/Customer/CustomerBenefit.tsx diff --git a/clients/apps/web/src/components/Customer/CustomerBenefit.tsx b/clients/apps/web/src/components/Customer/CustomerBenefit.tsx new file mode 100644 index 0000000000..1f01d9f98f --- /dev/null +++ b/clients/apps/web/src/components/Customer/CustomerBenefit.tsx @@ -0,0 +1,178 @@ +// https://github.com/polarsource/polar/issues/6167 +'use client' + +import { useCustomerBenefitGrantsList } from '@/hooks/queries/benefits' +import { + DataTablePaginationState, + 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 = ({ + customer, + organization, +}) => { + const [pagination, setPagination] = React.useState({ + pageIndex: 0, + pageSize: 10, + }) + const [sorting, setSorting] = React.useState([]) + + const { data: benefitGrants, isLoading } = useCustomerBenefitGrantsList({ + customerId: customer.id, + organizationId: organization.id, + page: pagination.pageIndex + 1, + limit: pagination.pageSize, + }) + + return ( +
+

Customer Benefits

+ {benefitGrants?.items.length === 0 && !isLoading ? ( +
+

This customer has no benefit grants.

+
+ ) : ( + ( +
+
+
+ {grant.benefit.description} +
+
+ {grant.benefit.type.replace('_', ' ').split(' ').map(word => + word.charAt(0).toUpperCase() + word.slice(1) + ).join(' ')} +
+
+
+ ), + }, + { + 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 ( + + + + + {statusDescription[status]} + + ) + }, + }, + { + accessorKey: 'created_at', + header: 'Granted', + cell: ({ row: { original: grant } }: { row: { original: schemas['BenefitGrant'] & { benefit: schemas['Benefit'] } } }) => ( + + ), + }, + { + accessorKey: 'revoked_at', + header: 'Revoked', + cell: ({ row: { original: grant } }: { row: { original: schemas['BenefitGrant'] & { benefit: schemas['Benefit'] } } }) => + grant.revoked_at ? ( + + ) : ( + + ), + }, + { + accessorKey: 'order', + header: 'Order', + cell: ({ row: { original: grant } }: { row: { original: schemas['BenefitGrant'] & { benefit: schemas['Benefit'] } } }) => + grant.order_id ? ( + + + + ) : ( + + ), + }, + { + accessorKey: 'subscription', + header: 'Subscription', + cell: ({ row: { original: grant } }: { row: { original: schemas['BenefitGrant'] & { benefit: schemas['Benefit'] } } }) => + grant.subscription_id ? ( + + + + ) : ( + + ), + }, + ]} + /> + )} +
+ ) +} diff --git a/clients/apps/web/src/components/Customer/CustomerPage.tsx b/clients/apps/web/src/components/Customer/CustomerPage.tsx index 64db0f9551..3e10503b2a 100644 --- a/clients/apps/web/src/components/Customer/CustomerPage.tsx +++ b/clients/apps/web/src/components/Customer/CustomerPage.tsx @@ -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' @@ -68,6 +69,7 @@ export const CustomerPage: React.FC = ({ Overview Events Usage + Benefits = ({ + + + ) } diff --git a/clients/apps/web/src/hooks/queries/benefits.ts b/clients/apps/web/src/hooks/queries/benefits.ts index aff45ed8f3..0047c79de7 100644 --- a/clients/apps/web/src/hooks/queries/benefits.ts +++ b/clients/apps/web/src/hooks/queries/benefits.ts @@ -203,3 +203,79 @@ export const useBenefitGrants = ({ }, retry: defaultRetry, }); + +export const useCustomerBenefitGrantsList = ({ + customerId, + organizationId, + limit = 30, + page = 1, +}: { + customerId: string; + organizationId: string; + limit?: number; + page?: number; +}) => + useQuery({ + queryKey: [ + "customer", + "benefit_grants", + customerId, + organizationId, + { page, limit }, + ], + queryFn: async () => { + // We need to get all benefits for the organization first, then get grants for each benefit filtered by customer + const benefitsResponse = await unwrap( + api.GET("/v1/benefits/", { + params: { + query: { + organization_id: organizationId, + limit: 100, // Get all benefits + }, + }, + }), + ); + + // Collect all grants for this customer across all benefits + 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, // Get all grants for this customer + }, + }, + }), + ); + // Add benefit information to each grant + const grantsWithBenefit = grantsResponse.items.map(grant => ({ + ...grant, + benefit, + })); + allGrants.push(...grantsWithBenefit); + } + + // Sort by created_at descending by default + const sortedGrants = allGrants.sort((a, b) => + new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ); + + // Apply pagination + const startIndex = (page - 1) * limit; + const endIndex = startIndex + limit; + const paginatedGrants = sortedGrants.slice(startIndex, endIndex); + + return { + items: paginatedGrants, + pagination: { + total_count: sortedGrants.length, + max_page: Math.ceil(sortedGrants.length / limit), + }, + }; + }, + retry: defaultRetry, + }); From f2b24d03e33d7f07f12b4034c5ed12162b51bffa Mon Sep 17 00:00:00 2001 From: Manas Kenge Date: Sun, 14 Sep 2025 12:29:13 +0530 Subject: [PATCH 2/2] update empty status --- .../components/Customer/CustomerBenefit.tsx | 32 +++++++------------ .../apps/web/src/hooks/queries/benefits.ts | 30 +++-------------- 2 files changed, 17 insertions(+), 45 deletions(-) diff --git a/clients/apps/web/src/components/Customer/CustomerBenefit.tsx b/clients/apps/web/src/components/Customer/CustomerBenefit.tsx index 1f01d9f98f..c88e68399a 100644 --- a/clients/apps/web/src/components/Customer/CustomerBenefit.tsx +++ b/clients/apps/web/src/components/Customer/CustomerBenefit.tsx @@ -1,11 +1,7 @@ -// https://github.com/polarsource/polar/issues/6167 'use client' import { useCustomerBenefitGrantsList } from '@/hooks/queries/benefits' -import { - DataTablePaginationState, - DataTableSortingState, -} from '@/utils/datatable' +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' @@ -29,36 +25,32 @@ export const CustomerBenefit: React.FC = ({ customer, organization, }) => { - const [pagination, setPagination] = React.useState({ - pageIndex: 0, - pageSize: 10, - }) const [sorting, setSorting] = React.useState([]) const { data: benefitGrants, isLoading } = useCustomerBenefitGrantsList({ customerId: customer.id, organizationId: organization.id, - page: pagination.pageIndex + 1, - limit: pagination.pageSize, }) return (

Customer Benefits

- {benefitGrants?.items.length === 0 && !isLoading ? ( -
-

This customer has no benefit grants.

+ {!isLoading && benefitGrants?.length === 0 && ( +
+
+

No benefits granted

+

+ This customer has no benefit grants. +

+
- ) : ( + )} + {(isLoading || (benefitGrants && benefitGrants.length > 0)) && ( useQuery({ queryKey: [ @@ -221,24 +217,21 @@ export const useCustomerBenefitGrantsList = ({ "benefit_grants", customerId, organizationId, - { page, limit }, ], queryFn: async () => { - // We need to get all benefits for the organization first, then get grants for each benefit filtered by customer const benefitsResponse = await unwrap( api.GET("/v1/benefits/", { params: { query: { organization_id: organizationId, - limit: 100, // Get all benefits + limit: 100, }, }, }), ); - // Collect all grants for this customer across all benefits const allGrants: (schemas['BenefitGrant'] & { benefit: schemas['Benefit'] })[] = []; - + for (const benefit of benefitsResponse.items) { const grantsResponse = await unwrap( api.GET("/v1/benefits/{id}/grants", { @@ -246,12 +239,11 @@ export const useCustomerBenefitGrantsList = ({ path: { id: benefit.id }, query: { customer_id: customerId, - limit: 1000, // Get all grants for this customer + limit: 1000, }, }, }), ); - // Add benefit information to each grant const grantsWithBenefit = grantsResponse.items.map(grant => ({ ...grant, benefit, @@ -259,23 +251,11 @@ export const useCustomerBenefitGrantsList = ({ allGrants.push(...grantsWithBenefit); } - // Sort by created_at descending by default - const sortedGrants = allGrants.sort((a, b) => + const sortedGrants = allGrants.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime() ); - // Apply pagination - const startIndex = (page - 1) * limit; - const endIndex = startIndex + limit; - const paginatedGrants = sortedGrants.slice(startIndex, endIndex); - - return { - items: paginatedGrants, - pagination: { - total_count: sortedGrants.length, - max_page: Math.ceil(sortedGrants.length / limit), - }, - }; + return sortedGrants; }, retry: defaultRetry, });