Skip to content

Commit

Permalink
[AC-1911] Clients: Create components to manage client organization se…
Browse files Browse the repository at this point in the history
…at allocation (#8505)

* implementing the clients changes

* resolve pr comments on message.json

* moved the method to billing-api.service

* move the request and response files to billing folder

* remove the adding existing orgs

* resolve the routing issue

* resolving the pr comments

* code owner changes

* fix the assignedseat

* resolve the warning message

* resolve the error on update

* passing the right id

* resolve the unassign value

* removed unused logservice

* Adding the loader on submit button
  • Loading branch information
cyprain-okeke committed Apr 2, 2024
1 parent b9771c1 commit 9956f02
Show file tree
Hide file tree
Showing 16 changed files with 575 additions and 9 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -61,6 +61,7 @@ apps/web/src/app/billing @bitwarden/team-billing-dev
libs/angular/src/billing @bitwarden/team-billing-dev
libs/common/src/billing @bitwarden/team-billing-dev
libs/billing @bitwarden/team-billing-dev
bitwarden_license/bit-web/src/app/billing @bitwarden/team-billing-dev

## Platform team files ##
apps/browser/src/platform @bitwarden/team-platform-dev
Expand Down
36 changes: 36 additions & 0 deletions apps/web/src/locales/en/messages.json
Expand Up @@ -4956,6 +4956,9 @@
"addExistingOrganization": {
"message": "Add existing organization"
},
"addNewOrganization": {
"message": "Add new organization"
},
"myProvider": {
"message": "My Provider"
},
Expand Down Expand Up @@ -7642,5 +7645,38 @@
},
"items": {
"message": "Items"
},
"assignedSeats": {
"message": "Assigned seats"
},
"assigned": {
"message": "Assigned"
},
"used": {
"message": "Used"
},
"remaining": {
"message": "Remaining"
},
"unlinkOrganization": {
"message": "Unlink organization"
},
"manageSeats": {
"message": "MANAGE SEATS"
},
"manageSeatsDescription": {
"message": "Adjustments to seats will be reflected in the next billing cycle."
},
"unassignedSeatsDescription": {
"message": "Unassigned subscription seats"
},
"purchaseSeatDescription": {
"message": "Additional seats purchased"
},
"assignedSeatCannotUpdate": {
"message": "Assigned Seats can not be updated. Please contact your organization owner for assistance."
},
"subscriptionUpdateFailed": {
"message": "Subscription update failed"
}
}
@@ -1,5 +1,5 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { first } from "rxjs/operators";

Expand All @@ -13,6 +13,8 @@ import { ProviderUserType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
import { PlanType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
Expand Down Expand Up @@ -50,8 +52,14 @@ export class ClientsComponent implements OnInit {
protected actionPromise: Promise<unknown>;
private pagedClientsCount = 0;

protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableConsolidatedBilling,
false,
);

constructor(
private route: ActivatedRoute,
private router: Router,
private providerService: ProviderService,
private apiService: ApiService,
private searchService: SearchService,
Expand All @@ -64,20 +72,29 @@ export class ClientsComponent implements OnInit {
private organizationService: OrganizationService,
private organizationApiService: OrganizationApiServiceAbstraction,
private dialogService: DialogService,
private configService: ConfigService,
) {}

async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;

await this.load();
const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$);

if (enableConsolidatedBilling) {
await this.router.navigate(["../manage-client-organizations"], { relativeTo: this.route });
} else {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;

/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
this.searchText = qParams.search;
await this.load();

/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
this.searchText = qParams.search;
});
});
});
}
}

async load() {
Expand Down
Expand Up @@ -4,7 +4,11 @@
<bit-icon [icon]="logo"></bit-icon>
</a>

<bit-nav-item icon="bwi-bank" [text]="'clients' | i18n" route="clients"></bit-nav-item>
<bit-nav-item
icon="bwi-bank"
[text]="'clients' | i18n"
[route]="(enableConsolidatedBilling$ | async) ? 'manage-client-organizations' : 'clients'"
></bit-nav-item>
<bit-nav-group icon="bwi-sliders" [text]="'manage' | i18n" route="manage" *ngIf="showManageTab">
<bit-nav-item
[text]="'people' | i18n"
Expand Down
Expand Up @@ -37,6 +37,11 @@ export class ProvidersLayoutComponent {
false,
);

protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableConsolidatedBilling,
false,
);

constructor(
private route: ActivatedRoute,
private providerService: ProviderService,
Expand Down
Expand Up @@ -7,6 +7,8 @@ import { ProvidersComponent } from "@bitwarden/web-vault/app/admin-console/provi
import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component";
import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component";

import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component";

import { ClientsComponent } from "./clients/clients.component";
import { CreateOrganizationComponent } from "./clients/create-organization.component";
import { ProviderPermissionsGuard } from "./guards/provider-permissions.guard";
Expand Down Expand Up @@ -64,6 +66,11 @@ const routes: Routes = [
{ path: "", pathMatch: "full", redirectTo: "clients" },
{ path: "clients/create", component: CreateOrganizationComponent },
{ path: "clients", component: ClientsComponent, data: { titleId: "clients" } },
{
path: "manage-client-organizations",
component: ManageClientOrganizationsComponent,
data: { titleId: "manage-client-organizations" },
},
{
path: "manage",
children: [
Expand Down
Expand Up @@ -8,6 +8,9 @@ import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing";
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
import { OssModule } from "@bitwarden/web-vault/app/oss.module";

import { ManageClientOrganizationSubscriptionComponent } from "../../billing/providers/clients/manage-client-organization-subscription.component";
import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component";

import { AddOrganizationComponent } from "./clients/add-organization.component";
import { ClientsComponent } from "./clients/clients.component";
import { CreateOrganizationComponent } from "./clients/create-organization.component";
Expand Down Expand Up @@ -50,6 +53,8 @@ import { SetupComponent } from "./setup/setup.component";
SetupComponent,
SetupProviderComponent,
UserAddEditComponent,
ManageClientOrganizationsComponent,
ManageClientOrganizationSubscriptionComponent,
],
providers: [WebProviderService, ProviderPermissionsGuard],
})
Expand Down
@@ -0,0 +1,49 @@
<bit-dialog dialogSize="large" [loading]="loading">
<span bitDialogTitle>
{{ "manageSeats" | i18n }}
<small class="tw-text-muted" *ngIf="clientName">{{ clientName }}</small>
</span>
<div bitDialogContent>
<p>
{{ "manageSeatsDescription" | i18n }}
</p>
<bit-form-field>
<bit-label>
{{ "assignedSeats" | i18n }}
</bit-label>
<input
id="assignedSeats"
type="number"
appAutoFocus
bitInput
required
[(ngModel)]="assignedSeats"
/>
</bit-form-field>
<ng-container *ngIf="remainingOpenSeats > 0">
<p>
<small class="tw-text-muted">{{ unassignedSeats }}</small>
<small class="tw-text-muted">{{ "unassignedSeatsDescription" | i18n }}</small>
</p>
<p>
<small class="tw-text-muted">{{ AdditionalSeatPurchased }}</small>
<small class="tw-text-muted">{{ "purchaseSeatDescription" | i18n }}</small>
</p>
</ng-container>
</div>
<ng-container bitDialogFooter>
<button
type="submit"
bitButton
buttonType="primary"
bitFormButton
(click)="updateSubscription(assignedSeats)"
>
<i class="bwi bwi-refresh bwi-fw" aria-hidden="true"></i>
{{ "save" | i18n }}
</button>
<button bitButton type="button" buttonType="secondary" bitDialogClose>
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>
@@ -0,0 +1,115 @@
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";

import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
import { ProviderSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/provider-subscription-update.request";
import { Plans } from "@bitwarden/common/billing/models/response/provider-subscription-response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components";

type ManageClientOrganizationDialogParams = {
organization: ProviderOrganizationOrganizationDetailsResponse;
};

@Component({
templateUrl: "manage-client-organization-subscription.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class ManageClientOrganizationSubscriptionComponent implements OnInit {
loading = true;
providerOrganizationId: string;
providerId: string;

clientName: string;
assignedSeats: number;
unassignedSeats: number;
planName: string;
AdditionalSeatPurchased: number;
remainingOpenSeats: number;

constructor(
public dialogRef: DialogRef,
@Inject(DIALOG_DATA) protected data: ManageClientOrganizationDialogParams,
private billingApiService: BillingApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
) {
this.providerOrganizationId = data.organization.id;
this.providerId = data.organization.providerId;
this.clientName = data.organization.organizationName;
this.assignedSeats = data.organization.seats;
this.planName = data.organization.plan;
}

async ngOnInit() {
try {
const response = await this.billingApiService.getProviderClientSubscriptions(this.providerId);
this.AdditionalSeatPurchased = this.getPurchasedSeatsByPlan(this.planName, response.plans);
const seatMinimum = this.getProviderSeatMinimumByPlan(this.planName, response.plans);
const assignedByPlan = this.getAssignedByPlan(this.planName, response.plans);
this.remainingOpenSeats = seatMinimum - assignedByPlan;
this.unassignedSeats = Math.abs(this.remainingOpenSeats);
} catch (error) {
this.remainingOpenSeats = 0;
this.AdditionalSeatPurchased = 0;
}
this.loading = false;
}

async updateSubscription(assignedSeats: number) {
this.loading = true;
if (!assignedSeats) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("assignedSeatCannotUpdate"),
);
return;
}

const request = new ProviderSubscriptionUpdateRequest();
request.assignedSeats = assignedSeats;

await this.billingApiService.putProviderClientSubscriptions(
this.providerId,
this.providerOrganizationId,
request,
);
this.platformUtilsService.showToast("success", null, this.i18nService.t("subscriptionUpdated"));
this.loading = false;
this.dialogRef.close();
}

getPurchasedSeatsByPlan(planName: string, plans: Plans[]): number {
const plan = plans.find((plan) => plan.planName === planName);
if (plan) {
return plan.purchasedSeats;
} else {
return 0;
}
}

getAssignedByPlan(planName: string, plans: Plans[]): number {
const plan = plans.find((plan) => plan.planName === planName);
if (plan) {
return plan.assignedSeats;
} else {
return 0;
}
}

getProviderSeatMinimumByPlan(planName: string, plans: Plans[]) {
const plan = plans.find((plan) => plan.planName === planName);
if (plan) {
return plan.seatMinimum;
} else {
return 0;
}
}

static open(dialogService: DialogService, data: ManageClientOrganizationDialogParams) {
return dialogService.open(ManageClientOrganizationSubscriptionComponent, { data });
}
}

0 comments on commit 9956f02

Please sign in to comment.