1
0
mirror of https://github.com/bitwarden/web synced 2025-12-06 00:03:28 +00:00

org billing seat adjustments

This commit is contained in:
Kyle Spearrin
2018-07-17 12:07:52 -04:00
parent e4f12ed47f
commit e4a684ff10
8 changed files with 176 additions and 5 deletions

2
jslib

Submodule jslib updated: 4228277d23...1cb3447bdd

View File

@@ -52,6 +52,7 @@ import { UserAddEditComponent as OrgUserAddEditComponent } from './organizations
import { UserGroupsComponent as OrgUserGroupsComponent } from './organizations/manage/user-groups.component';
import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component';
import { AdjustSeatsComponent } from './organizations/settings/adjust-seats.component';
import { DeleteOrganizationComponent } from './organizations/settings/delete-organization.component';
import { OrganizationBillingComponent } from './organizations/settings/organization-billing.component';
import { SettingsComponent as OrgSettingComponent } from './organizations/settings/settings.component';
@@ -149,6 +150,7 @@ import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
AccountComponent,
AddEditComponent,
AdjustPaymentComponent,
AdjustSeatsComponent,
AdjustStorageComponent,
ApiActionDirective,
AppComponent,

View File

@@ -0,0 +1,26 @@
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="card-body">
<h3 class="card-body-header">{{(add ? 'addSeats' : 'removeSeats') | i18n}}</h3>
<div class="row">
<div class="form-group col-6">
<label for="seatAdjustment">{{(add ? 'seatsToAdd' : 'seatsToRemove') | i18n}}</label>
<input id="seatAdjustment" class="form-control" type="number" name="SeatAdjustment" [(ngModel)]="seatAdjustment" min="0"
step="1" required>
</div>
</div>
<div *ngIf="add" class="mb-3">
<strong>{{'total' | i18n}}:</strong> {{seatAdjustment || 0}} &times; {{seatPrice | currency:'$'}} = {{adjustedSeatTotal
| currency:'$'}} /{{interval | i18n}}
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin"></i>
<span>{{'submit' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
{{'cancel' | i18n}}
</button>
<small class="d-block text-muted mt-3">
{{(add ? 'seatsAddNote' : 'seatsRemoveNote') | i18n}}
</small>
</div>
</form>

View File

@@ -0,0 +1,58 @@
import {
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { SeatRequest } from 'jslib/models/request/seatRequest';
@Component({
selector: 'app-adjust-seats',
templateUrl: 'adjust-seats.component.html',
})
export class AdjustSeatsComponent {
@Input() seatPrice = 0;
@Input() add = true;
@Input() organizationId: string;
@Input() interval = 'year';
@Output() onAdjusted = new EventEmitter<number>();
@Output() onCanceled = new EventEmitter();
seatAdjustment = 0;
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService) { }
async submit() {
try {
const request = new SeatRequest();
request.seatAdjustment = this.seatAdjustment;
if (!this.add) {
request.seatAdjustment *= -1;
}
this.formPromise = this.apiService.postOrganizationSeat(this.organizationId, request);
await this.formPromise;
this.analytics.eventTrack.next({ action: this.add ? 'Added Seats' : 'Removed Seats' });
this.toasterService.popAsync('success', null,
this.i18nService.t('adjustedSeats', request.seatAdjustment.toString()));
this.onAdjusted.emit(this.seatAdjustment);
} catch { }
}
cancel() {
this.onCanceled.emit();
}
get adjustedSeatTotal(): number {
return this.seatAdjustment * this.seatAdjustment;
}
}

View File

@@ -90,6 +90,22 @@
<span>{{'cancelSubscription' | i18n}}</span>
</button>
</div>
<h2 class="spaced-header">{{'userSeats' | i18n}}</h2>
<p>{{'subscriptionUserSeats' | i18n : billing.seats}}</p>
<ng-container *ngIf="subscription && canAdjustSeats">
<div class="mt-3">
<div class="d-flex" *ngIf="!showAdjustSeats">
<button type="button" class="btn btn-outline-secondary" (click)="adjustSeats(true)">
{{'addSeats' | i18n}}
</button>
<button type="button" class="btn btn-outline-secondary ml-1" (click)="adjustSeats(false)">
{{'removeSeats' | i18n}}
</button>
</div>
<app-adjust-seats [seatPrice]="seatPrice" [add]="adjustSeatsAdd" [organizationId]="organizationId" [interval]="billingInterval"
(onAdjusted)="closeSeats(true)" (onCanceled)="closeSeats(false)" *ngIf="showAdjustSeats"></app-adjust-seats>
</div>
</ng-container>
<h2 class="spaced-header">{{'storage' | i18n}}</h2>
<p>{{'subscriptionStorage' | i18n : billing.maxStorageGb || 0 : billing.storageName || '0 MB'}}</p>
<div class="progress">

View File

@@ -30,6 +30,8 @@ export class OrganizationBillingComponent implements OnInit {
loading = false;
firstLoaded = false;
organizationId: string;
adjustSeatsAdd = true;
showAdjustSeats = false;
adjustStorageAdd = true;
showAdjustStorage = false;
showAdjustPayment = false;
@@ -146,6 +148,18 @@ export class OrganizationBillingComponent implements OnInit {
}
}
adjustSeats(add: boolean) {
this.adjustSeatsAdd = add;
this.showAdjustSeats = true;
}
closeSeats(load: boolean) {
this.showAdjustSeats = false;
if (load) {
this.load();
}
}
adjustStorage(add: boolean) {
this.adjustStorageAdd = add;
this.showAdjustStorage = true;
@@ -216,6 +230,23 @@ export class OrganizationBillingComponent implements OnInit {
}
get seatPrice() {
return 4;
switch (this.billing.planType) {
case PlanType.EnterpriseMonthly:
return 4;
case PlanType.EnterpriseAnnually:
return 3;
case PlanType.TeamsMonthly:
return 2.5;
case PlanType.TeamsAnnually:
return 2;
default:
return 0;
}
}
get canAdjustSeats() {
return this.billing.planType === PlanType.EnterpriseMonthly ||
this.billing.planType === PlanType.EnterpriseAnnually ||
this.billing.planType === PlanType.TeamsMonthly || this.billing.planType === PlanType.TeamsAnnually;
}
}

View File

@@ -5,7 +5,7 @@
<div class="form-group col-6">
<label for="storageAdjustment">{{(add ? 'gbStorageAdd' : 'gbStorageRemove') | i18n}}</label>
<input id="storageAdjustment" class="form-control" type="number" name="StroageGbAdjustment" [(ngModel)]="storageAdjustment"
min="0" max="99" step="1">
min="0" max="99" step="1" required>
</div>
</div>
<div *ngIf="add" class="mb-3">

View File

@@ -1445,10 +1445,10 @@
"message": "GB of Storage To Remove"
},
"storageAddNote": {
"message": "Adding storage to your plan will result in adjustments to your billing totals and immediately charge your payment method on file. The first charge will be prorated for the remainder of the current billing cycle."
"message": "Adding storage will result in adjustments to your billing totals and immediately charge your payment method on file. The first charge will be prorated for the remainder of the current billing cycle."
},
"storageRemoveNote": {
"message": "Removing storage will result in adjustments to your billing totals that will be prorated as credits to your next billing charge."
"message": "Removing storage will result in adjustments to your billing totals that will be prorated as credits toward your next billing charge."
},
"adjustedStorage": {
"message": "Adjusted $AMOUNT$ GB of storage.",
@@ -2222,5 +2222,43 @@
},
"enterInstallationId": {
"message": "Enter your installation id"
},
"addSeats": {
"message": "Add Seats",
"description": "Seat = User Seat"
},
"removeSeats": {
"message": "Remove Seats",
"description": "Seat = User Seat"
},
"subscriptionUserSeats": {
"message": "Your subscription allows for a total of $COUNT$ users.",
"placeholders": {
"count": {
"content": "$1",
"example": "50"
}
}
},
"seatsToAdd": {
"message": "Seats To Add"
},
"seatsToRemove": {
"message": "Seats To Remove"
},
"seatsAddNote": {
"message": "Adding user seats will result in adjustments to your billing totals and immediately charge your payment method on file. The first charge will be prorated for the remainder of the current billing cycle."
},
"seatsRemoveNote": {
"message": "Removing user seats will result in adjustments to your billing totals that will be prorated as credits toward your next billing charge."
},
"adjustedSeats": {
"message": "Adjusted $AMOUNT$ user seats.",
"placeholders": {
"amount": {
"content": "$1",
"example": "15"
}
}
}
}