Skip to content

Commit

Permalink
[PM-16664] Incorrect price value in "add new organization" modal (#12710
Browse files Browse the repository at this point in the history
)
  • Loading branch information
jonashendrickx authored Jan 22, 2025
1 parent e2629ee commit 8784602
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 57 deletions.
3 changes: 3 additions & 0 deletions apps/web/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -9298,6 +9298,9 @@
"monthPerMember": {
"message": "month per member"
},
"monthPerMemberBilledAnnually": {
"message": "month per member billed annually"
},
"seats": {
"message": "Seats"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<div class="tw-grid tw-grid-flow-col tw-auto-cols-[minmax(0,_2fr)] tw-gap-4 tw-mb-4">
<div
*ngFor="let planCard of planCards"
[ngClass]="getPlanCardContainerClasses(planCard.selected)"
[ngClass]="planCard.getContainerClasses()"
(click)="selectPlan(planCard.name)"
tabindex="0"
>
Expand All @@ -29,9 +29,11 @@
<div class="tw-pl-5 tw-py-4 tw-pr-4" [ngClass]="{ 'tw-pt-10': !planCard.selected }">
<h3 class="tw-text-2xl tw-font-bold tw-uppercase">{{ planCard.name }}</h3>
<span class="tw-text-2xl tw-font-semibold">{{
planCard.cost | currency: "$"
planCard.getMonthlyCost() | currency: "$"
}}</span>
<span class="tw-text-sm tw-font-bold">/ {{ "monthPerMember" | i18n }}</span>
<span class="tw-text-sm tw-font-bold"
>/ {{ planCard.getTimePerMemberLabel() | i18n }}</span
>
</div>
</div>
</div>
Expand All @@ -45,8 +47,8 @@ <h3 class="tw-text-2xl tw-font-bold tw-uppercase">{{ planCard.name }}</h3>
<input type="text" bitInput formControlName="organizationName" />
<bit-error-summary
*ngIf="
formGroup.get('organizationName').errors?.['maxLength'] &&
formGroup.get('organizationName').touched
formGroup.controls.organizationName.errors?.['maxLength'] &&
formGroup.controls.organizationName.touched
"
>
{{ "organizationNameMaxLength" | i18n }}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { BasePortalOutlet } from "@angular/cdk/portal";
import { Component, Inject, OnInit } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";

Expand All @@ -25,48 +24,42 @@ export enum CreateClientDialogResultType {

export const openCreateClientDialog = (
dialogService: DialogService,
dialogConfig: DialogConfig<CreateClientDialogParams>,
dialogConfig: DialogConfig<
CreateClientDialogParams,
DialogRef<CreateClientDialogResultType, unknown>,
BasePortalOutlet
>,
) =>
dialogService.open<CreateClientDialogResultType, CreateClientDialogParams>(
CreateClientDialogComponent,
dialogConfig,
);

type PlanCard = {
name: string;
cost: number;
type: PlanType;
plan: PlanResponse;
export class PlanCard {
readonly name: string;
private readonly cost: number;
readonly type: PlanType;
readonly plan: PlanResponse;
selected: boolean;
};

@Component({
templateUrl: "./create-client-dialog.component.html",
})
export class CreateClientDialogComponent implements OnInit {
protected discountPercentage: number;
protected formGroup = new FormGroup({
clientOwnerEmail: new FormControl<string>("", [Validators.required, Validators.email]),
organizationName: new FormControl<string>("", [Validators.required, Validators.maxLength(50)]),
seats: new FormControl<number>(null, [Validators.required, Validators.min(1)]),
});
protected loading = true;
protected planCards: PlanCard[];
protected ResultType = CreateClientDialogResultType;
constructor(name: string, cost: number, type: PlanType, plan: PlanResponse, selected: boolean) {
this.name = name;
this.cost = cost;
this.type = type;
this.plan = plan;
this.selected = selected;
}

private providerPlans: ProviderPlanResponse[];
getMonthlyCost(): number {
return this.plan.isAnnual ? this.cost / 12 : this.cost;
}

constructor(
private billingApiService: BillingApiServiceAbstraction,
@Inject(DIALOG_DATA) private dialogParams: CreateClientDialogParams,
private dialogRef: DialogRef<CreateClientDialogResultType>,
private i18nService: I18nService,
private toastService: ToastService,
private webProviderService: WebProviderService,
) {}
getTimePerMemberLabel(): string {
return this.plan.isAnnual ? "monthPerMemberBilledAnnually" : "monthPerMember";
}

protected getPlanCardContainerClasses(selected: boolean) {
switch (selected) {
getContainerClasses() {
switch (this.selected) {
case true: {
return [
"tw-group/plan-card-container",
Expand Down Expand Up @@ -97,6 +90,41 @@ export class CreateClientDialogComponent implements OnInit {
}
}
}
}

@Component({
templateUrl: "./create-client-dialog.component.html",
})
export class CreateClientDialogComponent implements OnInit {
protected discountPercentage: number | null | undefined;
protected formGroup = new FormGroup({
clientOwnerEmail: new FormControl<string>("", {
nonNullable: true,
validators: [Validators.required, Validators.email],
}),
organizationName: new FormControl<string>("", {
nonNullable: true,
validators: [Validators.required, Validators.maxLength(50)],
}),
seats: new FormControl<number>(1, {
nonNullable: true,
validators: [Validators.required, Validators.min(1)],
}),
});
protected loading = true;
protected planCards: PlanCard[] = [];
protected ResultType = CreateClientDialogResultType;

private providerPlans: ProviderPlanResponse[] = [];

constructor(
private billingApiService: BillingApiServiceAbstraction,
@Inject(DIALOG_DATA) private dialogParams: CreateClientDialogParams,
private dialogRef: DialogRef<CreateClientDialogResultType>,
private i18nService: I18nService,
private toastService: ToastService,
private webProviderService: WebProviderService,
) {}

async ngOnInit(): Promise<void> {
const response = await this.billingApiService.getProviderSubscription(
Expand All @@ -114,6 +142,10 @@ export class CreateClientDialogComponent implements OnInit {
const providerPlan = this.providerPlans[i];
const plan = this.dialogParams.plans.find((plan) => plan.type === providerPlan.type);

if (!plan) {
continue;
}

let planName: string;
switch (plan.productTier) {
case ProductTierType.Teams: {
Expand All @@ -124,23 +156,28 @@ export class CreateClientDialogComponent implements OnInit {
planName = this.i18nService.t("planNameEnterprise");
break;
}
default:
continue;
}

this.planCards.push({
name: planName,
cost: plan.PasswordManager.providerPortalSeatPrice * discountFactor,
type: plan.type,
plan: plan,
selected: i === 0,
});
this.planCards.push(
new PlanCard(
planName,
plan.PasswordManager.providerPortalSeatPrice * discountFactor,
plan.type,
plan,
i === 0,
),
);
}

this.loading = false;
}

protected selectPlan(name: string) {
this.planCards.find((planCard) => planCard.name === name).selected = true;
this.planCards.find((planCard) => planCard.name !== name).selected = false;
this.planCards.forEach((planCard) => {
planCard.selected = planCard.name === name;
});
}

submit = async () => {
Expand All @@ -152,17 +189,21 @@ export class CreateClientDialogComponent implements OnInit {

const selectedPlanCard = this.planCards.find((planCard) => planCard.selected);

if (!selectedPlanCard) {
return;
}

await this.webProviderService.createClientOrganization(
this.dialogParams.providerId,
this.formGroup.value.organizationName,
this.formGroup.value.clientOwnerEmail,
this.formGroup.controls.organizationName.value,
this.formGroup.controls.clientOwnerEmail.value,
selectedPlanCard.type,
this.formGroup.value.seats,
this.formGroup.controls.seats.value,
);

this.toastService.showToast({
variant: "success",
title: null,
title: "",
message: this.i18nService.t("createdNewClient"),
});

Expand All @@ -178,7 +219,7 @@ export class CreateClientDialogComponent implements OnInit {

const openSeats = selectedProviderPlan.seatMinimum - selectedProviderPlan.assignedSeats;

const unassignedSeats = openSeats - this.formGroup.value.seats;
const unassignedSeats = openSeats - this.formGroup.controls.seats.value;

return unassignedSeats > 0 ? unassignedSeats : 0;
}
Expand All @@ -191,22 +232,22 @@ export class CreateClientDialogComponent implements OnInit {
}

if (selectedProviderPlan.purchasedSeats > 0) {
return this.formGroup.value.seats;
return this.formGroup.controls.seats.value;
}

const additionalSeatsPurchased =
this.formGroup.value.seats +
this.formGroup.controls.seats.value +
selectedProviderPlan.assignedSeats -
selectedProviderPlan.seatMinimum;

return additionalSeatsPurchased > 0 ? additionalSeatsPurchased : 0;
}

private getSelectedProviderPlan(): ProviderPlanResponse {
private getSelectedProviderPlan(): ProviderPlanResponse | null {
if (this.loading || !this.planCards) {
return null;
}
const selectedPlan = this.planCards.find((planCard) => planCard.selected).plan;
return this.providerPlans.find((providerPlan) => providerPlan.planName === selectedPlan.name);
const selectedPlan = this.planCards.find((planCard) => planCard.selected)!.plan;
return this.providerPlans.find((providerPlan) => providerPlan.planName === selectedPlan.name)!;
}
}

0 comments on commit 8784602

Please sign in to comment.