1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-13 06:43:35 +00:00

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
7 changed files with 65 additions and 29 deletions

View File

@@ -1,5 +1,5 @@
<form [formGroup]="formGroup" [bitSubmit]="submit"> <form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog dialogSize="large"> <bit-dialog dialogSize="large" [loading]="loading">
<span bitDialogTitle class="tw-font-semibold"> <span bitDialogTitle class="tw-font-semibold">
{{ "newClientOrganization" | i18n }} {{ "newClientOrganization" | i18n }}
</span> </span>
@@ -49,11 +49,21 @@
</bit-form-field> </bit-form-field>
</div> </div>
<div class="tw-grid tw-grid-flow-col tw-grid-cols-2 tw-gap-4"> <div class="tw-grid tw-grid-flow-col tw-grid-cols-2 tw-gap-4">
<bit-form-field> <bit-form-field disableMargin>
<bit-label> <bit-label>
{{ "seats" | i18n }} {{ "seats" | i18n }}
</bit-label> </bit-label>
<input type="text" bitInput formControlName="seats" /> <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> </bit-form-field>
</div> </div>
</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 { Component, Inject, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms"; 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 { PlanType } from "@bitwarden/common/billing/enums";
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components";
import { DialogService } from "@bitwarden/components";
import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service";
@@ -33,6 +34,7 @@ type PlanCard = {
name: string; name: string;
cost: number; cost: number;
type: PlanType; type: PlanType;
plan: PlanResponse;
selected: boolean; selected: boolean;
}; };
@@ -41,20 +43,24 @@ type PlanCard = {
templateUrl: "./create-client-organization.component.html", templateUrl: "./create-client-organization.component.html",
}) })
export class CreateClientOrganizationComponent implements OnInit { export class CreateClientOrganizationComponent implements OnInit {
protected ResultType = CreateClientOrganizationResultType;
protected formGroup = this.formBuilder.group({ protected formGroup = this.formBuilder.group({
clientOwnerEmail: ["", [Validators.required, Validators.email]], clientOwnerEmail: ["", [Validators.required, Validators.email]],
organizationName: ["", Validators.required], organizationName: ["", Validators.required],
seats: [null, [Validators.required, Validators.min(1)]], seats: [null, [Validators.required, Validators.min(1)]],
}); });
protected loading = true;
protected planCards: PlanCard[]; protected planCards: PlanCard[];
protected ResultType = CreateClientOrganizationResultType;
private providerPlans: ProviderPlanResponse[];
constructor( constructor(
private billingApiService: BillingApiServiceAbstraction,
@Inject(DIALOG_DATA) private dialogParams: CreateClientOrganizationParams, @Inject(DIALOG_DATA) private dialogParams: CreateClientOrganizationParams,
private dialogRef: DialogRef<CreateClientOrganizationResultType>, private dialogRef: DialogRef<CreateClientOrganizationResultType>,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private toastService: ToastService,
private webProviderService: WebProviderService, private webProviderService: WebProviderService,
) {} ) {}
@@ -92,6 +98,11 @@ export class CreateClientOrganizationComponent implements OnInit {
} }
async ngOnInit(): Promise<void> { 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 teamsPlan = this.dialogParams.plans.find((plan) => plan.type === PlanType.TeamsMonthly);
const enterprisePlan = this.dialogParams.plans.find( const enterprisePlan = this.dialogParams.plans.find(
(plan) => plan.type === PlanType.EnterpriseMonthly, (plan) => plan.type === PlanType.EnterpriseMonthly,
@@ -102,15 +113,19 @@ export class CreateClientOrganizationComponent implements OnInit {
name: this.i18nService.t("planNameTeams"), name: this.i18nService.t("planNameTeams"),
cost: teamsPlan.PasswordManager.seatPrice * 0.65, // 35% off for MSPs, cost: teamsPlan.PasswordManager.seatPrice * 0.65, // 35% off for MSPs,
type: teamsPlan.type, type: teamsPlan.type,
plan: teamsPlan,
selected: true, selected: true,
}, },
{ {
name: this.i18nService.t("planNameEnterprise"), name: this.i18nService.t("planNameEnterprise"),
cost: enterprisePlan.PasswordManager.seatPrice * 0.65, // 35% off for MSPs, cost: enterprisePlan.PasswordManager.seatPrice * 0.65, // 35% off for MSPs,
type: enterprisePlan.type, type: enterprisePlan.type,
plan: enterprisePlan,
selected: false, selected: false,
}, },
]; ];
this.loading = false;
} }
protected selectPlan(name: string) { protected selectPlan(name: string) {
@@ -135,8 +150,23 @@ export class CreateClientOrganizationComponent implements OnInit {
this.formGroup.value.seats, 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); 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> <p>
{{ "manageSeatsDescription" | i18n }} {{ "manageSeatsDescription" | i18n }}
</p> </p>
<bit-form-field> <bit-form-field disableMargin>
<bit-label> <bit-label>
{{ "assignedSeats" | i18n }} {{ "assignedSeats" | i18n }}
</bit-label> </bit-label>
<input id="assignedSeats" type="number" bitInput required [(ngModel)]="assignedSeats" /> <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> </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> </div>
<ng-container bitDialogFooter> <ng-container bitDialogFooter>
<button <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 { 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 { 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 { 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
@@ -83,7 +83,7 @@ export class ManageClientOrganizationSubscriptionComponent implements OnInit {
this.dialogRef.close(); this.dialogRef.close();
} }
getPurchasedSeatsByPlan(planName: string, plans: Plans[]): number { getPurchasedSeatsByPlan(planName: string, plans: ProviderPlanResponse[]): number {
const plan = plans.find((plan) => plan.planName === planName); const plan = plans.find((plan) => plan.planName === planName);
if (plan) { if (plan) {
return plan.purchasedSeats; 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); const plan = plans.find((plan) => plan.planName === planName);
if (plan) { if (plan) {
return plan.assignedSeats; 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); const plan = plans.find((plan) => plan.planName === planName);
if (plan) { if (plan) {
return plan.seatMinimum; return plan.seatMinimum;

View File

@@ -158,6 +158,4 @@ export class ManageClientOrganizationsComponent extends BaseClientsComponent {
await this.load(); 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 { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
import { import {
Plans, ProviderPlanResponse,
ProviderSubscriptionResponse, ProviderSubscriptionResponse,
} from "@bitwarden/common/billing/models/response/provider-subscription-response"; } from "@bitwarden/common/billing/models/response/provider-subscription-response";
@@ -75,7 +75,7 @@ export class ProviderSubscriptionComponent {
return totalSeats > 1 ? totalSeats.toString() : ""; return totalSeats > 1 ? totalSeats.toString() : "";
} }
sumCost(plans: Plans[]): number { sumCost(plans: ProviderPlanResponse[]): number {
return plans.reduce((acc, plan) => acc + plan.cost, 0); return plans.reduce((acc, plan) => acc + plan.cost, 0);
} }

View File

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