Fix spacing for provider unassigned seats hint' (#9460)

This commit is contained in:
Alex Morask 2024-06-06 13:22:15 -04:00 committed by GitHub
parent c8eac6fa12
commit 7d12d1a74f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 65 additions and 29 deletions

View File

@ -1,5 +1,5 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog dialogSize="large">
<bit-dialog dialogSize="large" [loading]="loading">
<span bitDialogTitle class="tw-font-semibold">
{{ "newClientOrganization" | i18n }}
</span>
@ -49,11 +49,21 @@
</bit-form-field>
</div>
<div class="tw-grid tw-grid-flow-col tw-grid-cols-2 tw-gap-4">
<bit-form-field>
<bit-form-field disableMargin>
<bit-label>
{{ "seats" | i18n }}
</bit-label>
<input type="text" bitInput formControlName="seats" />
<bit-hint
class="tw-text-muted tw-grid tw-grid-flow-col tw-gap-1 tw-grid-cols-1 tw-grid-rows-2"
*ngIf="unassignedSeatsForSelectedPlan > 0"
>
<span class="tw-col-span-1"
>{{ unassignedSeatsForSelectedPlan }}
{{ "unassignedSeatsDescription" | i18n | lowercase }}</span
>
<span class="tw-col-span-1">0 {{ "purchaseSeatDescription" | i18n | lowercase }}</span>
</bit-hint>
</bit-form-field>
</div>
</div>

View File

@ -2,11 +2,12 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
import { PlanType } from "@bitwarden/common/billing/enums";
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
import { ProviderPlanResponse } 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";
import { DialogService, ToastService } from "@bitwarden/components";
import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service";
@ -33,6 +34,7 @@ type PlanCard = {
name: string;
cost: number;
type: PlanType;
plan: PlanResponse;
selected: boolean;
};
@ -41,20 +43,24 @@ type PlanCard = {
templateUrl: "./create-client-organization.component.html",
})
export class CreateClientOrganizationComponent implements OnInit {
protected ResultType = CreateClientOrganizationResultType;
protected formGroup = this.formBuilder.group({
clientOwnerEmail: ["", [Validators.required, Validators.email]],
organizationName: ["", Validators.required],
seats: [null, [Validators.required, Validators.min(1)]],
});
protected loading = true;
protected planCards: PlanCard[];
protected ResultType = CreateClientOrganizationResultType;
private providerPlans: ProviderPlanResponse[];
constructor(
private billingApiService: BillingApiServiceAbstraction,
@Inject(DIALOG_DATA) private dialogParams: CreateClientOrganizationParams,
private dialogRef: DialogRef<CreateClientOrganizationResultType>,
private formBuilder: FormBuilder,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private toastService: ToastService,
private webProviderService: WebProviderService,
) {}
@ -92,6 +98,11 @@ export class CreateClientOrganizationComponent implements OnInit {
}
async ngOnInit(): Promise<void> {
const subscription = await this.billingApiService.getProviderSubscription(
this.dialogParams.providerId,
);
this.providerPlans = subscription?.plans ?? [];
const teamsPlan = this.dialogParams.plans.find((plan) => plan.type === PlanType.TeamsMonthly);
const enterprisePlan = this.dialogParams.plans.find(
(plan) => plan.type === PlanType.EnterpriseMonthly,
@ -102,15 +113,19 @@ export class CreateClientOrganizationComponent implements OnInit {
name: this.i18nService.t("planNameTeams"),
cost: teamsPlan.PasswordManager.seatPrice * 0.65, // 35% off for MSPs,
type: teamsPlan.type,
plan: teamsPlan,
selected: true,
},
{
name: this.i18nService.t("planNameEnterprise"),
cost: enterprisePlan.PasswordManager.seatPrice * 0.65, // 35% off for MSPs,
type: enterprisePlan.type,
plan: enterprisePlan,
selected: false,
},
];
this.loading = false;
}
protected selectPlan(name: string) {
@ -135,8 +150,23 @@ export class CreateClientOrganizationComponent implements OnInit {
this.formGroup.value.seats,
);
this.platformUtilsService.showToast("success", null, this.i18nService.t("createdNewClient"));
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("createdNewClient"),
});
this.dialogRef.close(this.ResultType.Submitted);
};
protected get unassignedSeatsForSelectedPlan(): number {
if (this.loading || !this.planCards) {
return 0;
}
const selectedPlan = this.planCards.find((planCard) => planCard.selected).plan;
const selectedProviderPlan = this.providerPlans.find(
(providerPlan) => providerPlan.planName === selectedPlan.name,
);
return selectedProviderPlan.seatMinimum - selectedProviderPlan.assignedSeats;
}
}

View File

@ -7,22 +7,20 @@
<p>
{{ "manageSeatsDescription" | i18n }}
</p>
<bit-form-field>
<bit-form-field disableMargin>
<bit-label>
{{ "assignedSeats" | i18n }}
</bit-label>
<input id="assignedSeats" type="number" bitInput required [(ngModel)]="assignedSeats" />
<bit-hint class="tw-text-muted" *ngIf="remainingOpenSeats > 0">
<div class="tw-grid tw-grid-flow-col tw-gap-1 tw-grid-cols-1 tw-grid-rows-2">
<span class="tw-col-span-1"
>{{ unassignedSeats }} {{ "unassignedSeatsDescription" | i18n | lowercase }}</span
>
<span class="tw-col-span-1">0 {{ "purchaseSeatDescription" | i18n | lowercase }}</span>
</div>
</bit-hint>
</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

View File

@ -4,7 +4,7 @@ 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 { UpdateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/update-client-organization.request";
import { Plans } from "@bitwarden/common/billing/models/response/provider-subscription-response";
import { ProviderPlanResponse } 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";
@ -83,7 +83,7 @@ export class ManageClientOrganizationSubscriptionComponent implements OnInit {
this.dialogRef.close();
}
getPurchasedSeatsByPlan(planName: string, plans: Plans[]): number {
getPurchasedSeatsByPlan(planName: string, plans: ProviderPlanResponse[]): number {
const plan = plans.find((plan) => plan.planName === planName);
if (plan) {
return plan.purchasedSeats;
@ -92,7 +92,7 @@ export class ManageClientOrganizationSubscriptionComponent implements OnInit {
}
}
getAssignedByPlan(planName: string, plans: Plans[]): number {
getAssignedByPlan(planName: string, plans: ProviderPlanResponse[]): number {
const plan = plans.find((plan) => plan.planName === planName);
if (plan) {
return plan.assignedSeats;
@ -101,7 +101,7 @@ export class ManageClientOrganizationSubscriptionComponent implements OnInit {
}
}
getProviderSeatMinimumByPlan(planName: string, plans: Plans[]) {
getProviderSeatMinimumByPlan(planName: string, plans: ProviderPlanResponse[]) {
const plan = plans.find((plan) => plan.planName === planName);
if (plan) {
return plan.seatMinimum;

View File

@ -158,6 +158,4 @@ export class ManageClientOrganizationsComponent extends BaseClientsComponent {
await this.load();
};
protected readonly openManageClientOrganizationNameDialog =
openManageClientOrganizationNameDialog;
}

View File

@ -4,7 +4,7 @@ import { Subject, concatMap, takeUntil } from "rxjs";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
import {
Plans,
ProviderPlanResponse,
ProviderSubscriptionResponse,
} from "@bitwarden/common/billing/models/response/provider-subscription-response";
@ -75,7 +75,7 @@ export class ProviderSubscriptionComponent {
return totalSeats > 1 ? totalSeats.toString() : "";
}
sumCost(plans: Plans[]): number {
sumCost(plans: ProviderPlanResponse[]): number {
return plans.reduce((acc, plan) => acc + plan.cost, 0);
}

View File

@ -4,11 +4,11 @@ export class ProviderSubscriptionResponse extends BaseResponse {
status: string;
currentPeriodEndDate: Date;
discountPercentage?: number | null;
plans: ProviderPlanResponse[] = [];
collectionMethod: string;
unpaidPeriodEndDate?: string;
gracePeriod?: number | null;
suspensionDate?: string;
plans: Plans[] = [];
constructor(response: any) {
super(response);
@ -21,12 +21,12 @@ export class ProviderSubscriptionResponse extends BaseResponse {
this.suspensionDate = this.getResponseProperty("suspensionDate");
const plans = this.getResponseProperty("plans");
if (plans != null) {
this.plans = plans.map((i: any) => new Plans(i));
this.plans = plans.map((i: any) => new ProviderPlanResponse(i));
}
}
}
export class Plans extends BaseResponse {
export class ProviderPlanResponse extends BaseResponse {
planName: string;
seatMinimum: number;
assignedSeats: number;