diff --git a/packages/twenty-front/src/generated-admin/graphql.ts b/packages/twenty-front/src/generated-admin/graphql.ts index 93c051fbb9..76656a890d 100644 --- a/packages/twenty-front/src/generated-admin/graphql.ts +++ b/packages/twenty-front/src/generated-admin/graphql.ts @@ -117,6 +117,7 @@ export type AdminPanelWorkspaceBilling = { creditBalance?: Maybe; stripeCustomerId?: Maybe; subscription?: Maybe; + usage?: Maybe; }; export type AdminPanelWorkspaceSubscription = { @@ -146,6 +147,17 @@ export type AdminPanelWorkspaceSubscriptionItem = { unitAmount?: Maybe; }; +export type AdminPanelWorkspaceUsage = { + __typename?: 'AdminPanelWorkspaceUsage'; + grantedCredits: Scalars['Float']['output']; + periodEnd: Scalars['DateTime']['output']; + periodStart: Scalars['DateTime']['output']; + remainingCredits: Scalars['Float']['output']; + rolloverCredits: Scalars['Float']['output']; + totalGrantedCredits: Scalars['Float']['output']; + usedCredits: Scalars['Float']['output']; +}; + export type AdminWorkspaceChatThread = { __typename?: 'AdminWorkspaceChatThread'; conversationSize: Scalars['Int']['output']; @@ -1096,7 +1108,7 @@ export type WorkspaceBillingAdminPanelQueryVariables = Exact<{ }>; -export type WorkspaceBillingAdminPanelQuery = { __typename?: 'Query', workspaceBillingAdminPanel?: { __typename?: 'AdminPanelWorkspaceBilling', stripeCustomerId?: string | null, creditBalance?: number | null, subscription?: { __typename?: 'AdminPanelWorkspaceSubscription', stripeSubscriptionId: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, currency: string, planKey?: string | null, currentPeriodStart: string, currentPeriodEnd: string, trialStart?: string | null, trialEnd?: string | null, cancelAt?: string | null, canceledAt?: string | null, cancelAtPeriodEnd: boolean, items: Array<{ __typename?: 'AdminPanelWorkspaceSubscriptionItem', productName: string, productKey?: string | null, stripePriceId: string, quantity?: number | null, unitAmount?: number | null, includedCredits?: number | null }> } | null } | null }; +export type WorkspaceBillingAdminPanelQuery = { __typename?: 'Query', workspaceBillingAdminPanel?: { __typename?: 'AdminPanelWorkspaceBilling', stripeCustomerId?: string | null, creditBalance?: number | null, usage?: { __typename?: 'AdminPanelWorkspaceUsage', periodStart: string, periodEnd: string, usedCredits: number, grantedCredits: number, rolloverCredits: number, totalGrantedCredits: number, remainingCredits: number } | null, subscription?: { __typename?: 'AdminPanelWorkspaceSubscription', stripeSubscriptionId: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, currency: string, planKey?: string | null, currentPeriodStart: string, currentPeriodEnd: string, trialStart?: string | null, trialEnd?: string | null, cancelAt?: string | null, canceledAt?: string | null, cancelAtPeriodEnd: boolean, items: Array<{ __typename?: 'AdminPanelWorkspaceSubscriptionItem', productName: string, productKey?: string | null, stripePriceId: string, quantity?: number | null, unitAmount?: number | null, includedCredits?: number | null }> } | null } | null }; export type UserLookupAdminPanelQueryVariables = Exact<{ userIdentifier: Scalars['String']['input']; @@ -1235,7 +1247,7 @@ export const GetAdminWorkspaceChatThreadsDocument = {"kind":"Document","definiti export const GetServerAdminsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetServerAdmins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getServerAdmins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"canAccessFullAdminPanel"}},{"kind":"Field","name":{"kind":"Name","value":"canImpersonate"}}]}}]}}]} as unknown as DocumentNode; export const GetUpgradeStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUpgradeStatus"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getUpgradeStatus"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"inferredVersion"}},{"kind":"Field","name":{"kind":"Name","value":"health"}},{"kind":"Field","name":{"kind":"Name","value":"latestCommand"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"executedByVersion"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetVersionInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetVersionInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"versionInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currentVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}}]}}]}}]} as unknown as DocumentNode; -export const WorkspaceBillingAdminPanelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"WorkspaceBillingAdminPanel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceBillingAdminPanel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stripeCustomerId"}},{"kind":"Field","name":{"kind":"Name","value":"creditBalance"}},{"kind":"Field","name":{"kind":"Name","value":"subscription"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stripeSubscriptionId"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"interval"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"planKey"}},{"kind":"Field","name":{"kind":"Name","value":"currentPeriodStart"}},{"kind":"Field","name":{"kind":"Name","value":"currentPeriodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"trialStart"}},{"kind":"Field","name":{"kind":"Name","value":"trialEnd"}},{"kind":"Field","name":{"kind":"Name","value":"cancelAt"}},{"kind":"Field","name":{"kind":"Name","value":"canceledAt"}},{"kind":"Field","name":{"kind":"Name","value":"cancelAtPeriodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"productName"}},{"kind":"Field","name":{"kind":"Name","value":"productKey"}},{"kind":"Field","name":{"kind":"Name","value":"stripePriceId"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"unitAmount"}},{"kind":"Field","name":{"kind":"Name","value":"includedCredits"}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const WorkspaceBillingAdminPanelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"WorkspaceBillingAdminPanel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceBillingAdminPanel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stripeCustomerId"}},{"kind":"Field","name":{"kind":"Name","value":"creditBalance"}},{"kind":"Field","name":{"kind":"Name","value":"usage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"periodStart"}},{"kind":"Field","name":{"kind":"Name","value":"periodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"usedCredits"}},{"kind":"Field","name":{"kind":"Name","value":"grantedCredits"}},{"kind":"Field","name":{"kind":"Name","value":"rolloverCredits"}},{"kind":"Field","name":{"kind":"Name","value":"totalGrantedCredits"}},{"kind":"Field","name":{"kind":"Name","value":"remainingCredits"}}]}},{"kind":"Field","name":{"kind":"Name","value":"subscription"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stripeSubscriptionId"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"interval"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"planKey"}},{"kind":"Field","name":{"kind":"Name","value":"currentPeriodStart"}},{"kind":"Field","name":{"kind":"Name","value":"currentPeriodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"trialStart"}},{"kind":"Field","name":{"kind":"Name","value":"trialEnd"}},{"kind":"Field","name":{"kind":"Name","value":"cancelAt"}},{"kind":"Field","name":{"kind":"Name","value":"canceledAt"}},{"kind":"Field","name":{"kind":"Name","value":"cancelAtPeriodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"productName"}},{"kind":"Field","name":{"kind":"Name","value":"productKey"}},{"kind":"Field","name":{"kind":"Name","value":"stripePriceId"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"unitAmount"}},{"kind":"Field","name":{"kind":"Name","value":"includedCredits"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const UserLookupAdminPanelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserLookupAdminPanel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userLookupAdminPanel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"userIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userIdentifier"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserInfoFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"totalUsers"}},{"kind":"Field","name":{"kind":"Name","value":"activationStatus"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"allowImpersonation"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceUrls"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customUrl"}},{"kind":"Field","name":{"kind":"Name","value":"subdomainUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"users"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featureFlags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserInfoFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"UserInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; export const WorkspaceLookupAdminPanelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"WorkspaceLookupAdminPanel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceLookupAdminPanel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserInfoFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"allowImpersonation"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"totalUsers"}},{"kind":"Field","name":{"kind":"Name","value":"activationStatus"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceUrls"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"customUrl"}},{"kind":"Field","name":{"kind":"Name","value":"subdomainUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"users"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featureFlags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserInfoFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"UserInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; export const DeleteJobsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteJobs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"jobIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteJobs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"queueName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"queueName"}}},{"kind":"Argument","name":{"kind":"Name","value":"jobIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"jobIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deletedCount"}},{"kind":"Field","name":{"kind":"Name","value":"results"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"jobId"}},{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceBillingContent.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceBillingContent.tsx index ea6e6f9bf8..50bd3244a6 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceBillingContent.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceBillingContent.tsx @@ -1,6 +1,7 @@ import { useQuery } from '@apollo/client/react'; import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; +import { useContext } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { Tag } from 'twenty-ui-deprecated/components'; import { @@ -8,6 +9,7 @@ import { IconBox, IconCalendarEvent, IconCalendarRepeat, + IconChartBar, IconCircleX, IconCoins, IconCreditCard, @@ -19,7 +21,10 @@ import { } from 'twenty-ui-deprecated/display'; import { Section } from 'twenty-ui-deprecated/layout'; import { type ThemeColor } from 'twenty-ui-deprecated/theme'; -import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants'; +import { + ThemeContext, + themeCssVariables, +} from 'twenty-ui-deprecated/theme-constants'; import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient'; import { GET_WORKSPACE_BILLING_ADMIN_PANEL } from '@/settings/admin-panel/graphql/queries/getWorkspaceBillingAdminPanel'; @@ -73,6 +78,10 @@ const StyledItemValue = styled.div` gap: ${themeCssVariables.spacing[2]}; `; +const StyledProgressBarContainer = styled.div` + margin-bottom: ${themeCssVariables.spacing[3]}; +`; + const STATUS_COLORS: Record = { [SubscriptionStatus.Active]: 'green', [SubscriptionStatus.Trialing]: 'blue', @@ -137,6 +146,7 @@ export const SettingsAdminWorkspaceBillingContent = ({ }: SettingsAdminWorkspaceBillingContentProps) => { const { t } = useLingui(); const { formatNumber } = useNumberFormat(); + const { theme } = useContext(ThemeContext); const apolloAdminClient = useApolloAdminClient(); const { data, loading } = useQuery( @@ -171,7 +181,10 @@ export const SettingsAdminWorkspaceBillingContent = ({ ); } - const { stripeCustomerId, creditBalance, subscription } = billing; + const { stripeCustomerId, creditBalance, subscription, usage } = billing; + + const formatCredits = (credits: number): string => + formatNumber(credits, { abbreviate: true, decimals: 2 }); const customerItems = [ { @@ -207,6 +220,39 @@ export const SettingsAdminWorkspaceBillingContent = ({ : null; const isTrialing = subscription?.status === SubscriptionStatus.Trialing; + const usageItems = isDefined(usage) + ? [ + { + Icon: IconChartBar, + label: t`Credits used`, + value: `${formatCredits(usage.usedCredits)} / ${formatCredits(usage.totalGrantedCredits)}`, + }, + ...(!isTrialing + ? [ + { + Icon: IconCoins, + label: t`Base credits`, + value: formatCredits(usage.grantedCredits), + }, + ] + : []), + ...(usage.rolloverCredits > 0 + ? [ + { + Icon: IconCoins, + label: t`Rollover credits`, + value: formatCredits(usage.rolloverCredits), + }, + ] + : []), + { + Icon: IconCalendarRepeat, + label: t`Usage period`, + value: formatPeriod(usage.periodStart, usage.periodEnd), + }, + ] + : []; + const formatItemValue = ( item: NonNullable['items'][number], ): string => { @@ -349,6 +395,24 @@ export const SettingsAdminWorkspaceBillingContent = ({ /> +
+ + {isDefined(usage) && ( + + )} +
+
AdminPanelWorkspaceSubscriptionDTO, { nullable: true }) subscription: AdminPanelWorkspaceSubscriptionDTO | null; + + @Field(() => AdminPanelWorkspaceUsageDTO, { nullable: true }) + usage: AdminPanelWorkspaceUsageDTO | null; } diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-usage.dto.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-usage.dto.ts new file mode 100644 index 0000000000..5e167792b9 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-usage.dto.ts @@ -0,0 +1,25 @@ +import { Field, Float, ObjectType } from '@nestjs/graphql'; + +@ObjectType('AdminPanelWorkspaceUsage') +export class AdminPanelWorkspaceUsageDTO { + @Field(() => Date) + periodStart: Date; + + @Field(() => Date) + periodEnd: Date; + + @Field(() => Float) + usedCredits: number; + + @Field(() => Float) + grantedCredits: number; + + @Field(() => Float) + rolloverCredits: number; + + @Field(() => Float) + totalGrantedCredits: number; + + @Field(() => Float) + remainingCredits: number; +} diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-billing.service.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-billing.service.ts index 090038f844..49d57817b1 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-billing.service.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-billing.service.ts @@ -1,14 +1,18 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, type Repository } from 'typeorm'; import { AdminPanelWorkspaceBillingDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-billing.dto'; +import { type AdminPanelWorkspaceUsageDTO } from 'src/engine/core-modules/admin-panel/dtos/admin-panel-workspace-usage.dto'; import { BillingCustomerEntity } from 'src/engine/core-modules/billing/entities/billing-customer.entity'; import { BillingPriceEntity } from 'src/engine/core-modules/billing/entities/billing-price.entity'; import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; +import { BillingUsageService } from 'src/engine/core-modules/billing/services/billing-usage.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { toDisplayCredits } from 'src/engine/core-modules/usage/utils/to-display-credits.util'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator'; import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository'; const CREDIT_BALANCE_MICRO_UNIT = 1_000_000; @@ -19,12 +23,17 @@ const KNOWN_PLAN_KEYS: ReadonlySet = new Set( @Injectable() export class AdminPanelBillingService { + private readonly logger = new Logger(AdminPanelBillingService.name); + constructor( @InjectWorkspaceScopedRepository(BillingCustomerEntity) private readonly billingCustomerRepository: WorkspaceScopedRepository, @InjectRepository(BillingPriceEntity) private readonly billingPriceRepository: Repository, + @InjectRepository(WorkspaceEntity) + private readonly workspaceRepository: Repository, private readonly billingSubscriptionService: BillingSubscriptionService, + private readonly billingUsageService: BillingUsageService, private readonly twentyConfigService: TwentyConfigService, ) {} @@ -57,9 +66,12 @@ export class AdminPanelBillingService { stripeCustomerId, creditBalance, subscription: null, + usage: null, }; } + const usage = await this.getWorkspaceUsage(workspaceId); + const items = subscription.billingSubscriptionItems ?? []; const priceIds = items.map((item) => item.stripePriceId); const prices = priceIds.length @@ -80,6 +92,7 @@ export class AdminPanelBillingService { return { stripeCustomerId, creditBalance, + usage, subscription: { stripeSubscriptionId: subscription.stripeSubscriptionId, status: subscription.status, @@ -112,4 +125,48 @@ export class AdminPanelBillingService { }, }; } + + private async getWorkspaceUsage( + workspaceId: string, + ): Promise { + const workspace = await this.workspaceRepository.findOne({ + where: { id: workspaceId }, + }); + + if (!workspace) { + return null; + } + + try { + const [usage] = + await this.billingUsageService.getResourceCreditProductUsage(workspace); + + if (!usage) { + return null; + } + + const usedCredits = toDisplayCredits(usage.usedCredits); + const grantedCredits = toDisplayCredits(usage.grantedCredits); + const rolloverCredits = toDisplayCredits(usage.rolloverCredits); + const totalGrantedCredits = toDisplayCredits(usage.totalGrantedCredits); + + return { + periodStart: usage.periodStart, + periodEnd: usage.periodEnd, + usedCredits, + grantedCredits, + rolloverCredits, + totalGrantedCredits, + remainingCredits: totalGrantedCredits - usedCredits, + }; + } catch (error) { + this.logger.warn( + `Failed to compute credit usage for workspace ${workspaceId}: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + + return null; + } + } }