[AC-1934] Clients: Create component to display provider subscription details (#9129)

* initial commit

* Make changes for provider billing details

* replace the hardcoded values with real data

* Apply discount on the displayed amount

* Fix the design issues base on the new design changes

* Fix the design space issue

* Remove unnecessary If statements

* Revert the change

* Remove unnecessary If statements

* Refactoring the discount calculation for easy understanding
This commit is contained in:
cyprain-okeke 2024-05-14 14:50:36 +01:00 committed by GitHub
parent 26c08123bb
commit 79a0b0d46d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 177 additions and 1 deletions

View File

@ -8192,5 +8192,20 @@
},
"updatedOrganizationName": {
"message": "Updated organization name"
},
"providerPlan": {
"message": "Managed Service Provider"
},
"orgSeats": {
"message": "Organization Seats"
},
"providerDiscount": {
"message": "$AMOUNT$% Discount",
"placeholders": {
"amount": {
"content": "$1",
"example": "2"
}
}
}
}

View File

@ -1 +1,83 @@
<app-header></app-header>
<bit-container>
<ng-container *ngIf="!firstLoaded && loading">
<i class="bwi bwi-spinner bwi-spin text-muted" title="{{ 'loading' | i18n }}"></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container *ngIf="subscription && firstLoaded">
<bit-callout type="warning" title="{{ 'canceled' | i18n }}" *ngIf="false">
{{ "subscriptionCanceled" | i18n }}</bit-callout
>
<dl class="tw-grid tw-grid-flow-col tw-grid-rows-2">
<dt>{{ "billingPlan" | i18n }}</dt>
<dd>{{ "providerPlan" | i18n }}</dd>
<ng-container *ngIf="subscription">
<dt>{{ "status" | i18n }}</dt>
<dd>
<span class="tw-capitalize">{{ subscription.status }}</span>
</dd>
<dt [ngClass]="{ 'tw-text-danger': isExpired }">{{ "nextCharge" | i18n }}</dt>
<dd [ngClass]="{ 'tw-text-danger': isExpired }">
{{ subscription.currentPeriodEndDate | date: "mediumDate" }}
</dd>
</ng-container>
</dl>
</ng-container>
<ng-container>
<div class="tw-flex-col">
<strong class="tw-block tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300 pb-2"
>{{ "details" | i18n }} &#160;<span
bitBadge
variant="success"
*ngIf="subscription.discountPercentage"
>{{ "providerDiscount" | i18n: subscription.discountPercentage }}</span
>
</strong>
<bit-table>
<ng-template body>
<ng-container *ngIf="subscription">
<tr bitRow *ngFor="let i of subscription.plans">
<td bitCell class="tw-pl-0 tw-py-3">
{{ getFormattedPlanName(i.planName) }} {{ "orgSeats" | i18n }} ({{
i.cadence.toLowerCase()
}}) {{ "&times;" }}{{ getFormattedSeatCount(i.seatMinimum, i.purchasedSeats) }}
@
{{
getFormattedCost(
i.cost,
i.seatMinimum,
i.purchasedSeats,
subscription.discountPercentage
) | currency: "$"
}}
</td>
<td bitCell class="tw-text-right tw-py-3">
{{ ((100 - subscription.discountPercentage) / 100) * i.cost | currency: "$" }} /{{
"month" | i18n
}}
<div>
<bit-hint class="tw-text-sm tw-line-through">
{{ i.cost | currency: "$" }} /{{ "month" | i18n }}
</bit-hint>
</div>
</td>
</tr>
<tr bitRow>
<td bitCell class="tw-pl-0 tw-py-3"></td>
<td bitCell class="tw-text-right">
<span class="tw-font-bold">Total:</span> {{ totalCost | currency: "$" }} /{{
"month" | i18n
}}
</td>
</tr>
</ng-container>
</ng-template>
</bit-table>
</div>
</ng-container>
</bit-container>

View File

@ -1,7 +1,86 @@
import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { Subject, concatMap, takeUntil } from "rxjs";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
import {
Plans,
ProviderSubscriptionResponse,
} from "@bitwarden/common/billing/models/response/provider-subscription-response";
@Component({
selector: "app-provider-subscription",
templateUrl: "./provider-subscription.component.html",
})
export class ProviderSubscriptionComponent {}
export class ProviderSubscriptionComponent {
subscription: ProviderSubscriptionResponse;
providerId: string;
firstLoaded = false;
loading: boolean;
private destroy$ = new Subject<void>();
totalCost: number;
currentDate = new Date();
constructor(
private billingApiService: BillingApiServiceAbstraction,
private route: ActivatedRoute,
) {}
async ngOnInit() {
this.route.params
.pipe(
concatMap(async (params) => {
this.providerId = params.providerId;
await this.load();
this.firstLoaded = true;
}),
takeUntil(this.destroy$),
)
.subscribe();
}
get isExpired() {
return this.subscription.status !== "active";
}
async load() {
if (this.loading) {
return;
}
this.loading = true;
this.subscription = await this.billingApiService.getProviderSubscription(this.providerId);
this.totalCost =
((100 - this.subscription.discountPercentage) / 100) * this.sumCost(this.subscription.plans);
this.loading = false;
}
getFormattedCost(
cost: number,
seatMinimum: number,
purchasedSeats: number,
discountPercentage: number,
): number {
const costPerSeat = cost / (seatMinimum + purchasedSeats);
const discountedCost = costPerSeat - (costPerSeat * discountPercentage) / 100;
return discountedCost;
}
getFormattedPlanName(planName: string): string {
const spaceIndex = planName.indexOf(" ");
return planName.substring(0, spaceIndex);
}
getFormattedSeatCount(seatMinimum: number, purchasedSeats: number): string {
const totalSeats = seatMinimum + purchasedSeats;
return totalSeats > 1 ? totalSeats.toString() : "";
}
sumCost(plans: Plans[]): number {
return plans.reduce((acc, plan) => acc + plan.cost, 0);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}