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:
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -158,6 +158,4 @@ export class ManageClientOrganizationsComponent extends BaseClientsComponent {
|
|||||||
|
|
||||||
await this.load();
|
await this.load();
|
||||||
};
|
};
|
||||||
protected readonly openManageClientOrganizationNameDialog =
|
|
||||||
openManageClientOrganizationNameDialog;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user