Skip to content

Commit

Permalink
implement "my profile" (alfio-event#78)
Browse files Browse the repository at this point in the history
* initial work for displaying user profile

* update profile

* add guard for preventing users to access my-profile
  • Loading branch information
cbellone authored May 30, 2021
1 parent 2a28d88 commit b0c4268
Show file tree
Hide file tree
Showing 15 changed files with 234 additions and 20 deletions.
5 changes: 4 additions & 1 deletion src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { RemoveEventCssGuard } from './remove-event-css.guard';
import { SubscriptionDisplayComponent } from './subscription-display/subscription-display.component';
import { SuccessSubscriptionComponent } from './reservation/success-subscription/success-subscription.component';
import { MyOrdersComponent } from './my-orders/my-orders.component';
import { MyProfileComponent } from './my-profile/my-profile.component';
import {UserLoggedInGuard} from './user-logged-in.guard';

const eventReservationsGuard = [EventGuard, LanguageGuard, ReservationGuard];
const eventData = {type: 'event', publicIdentifierParameter: 'eventShortName'};
Expand Down Expand Up @@ -61,7 +63,8 @@ const routes: Routes = [
{ path: 'view', component: ViewTicketComponent, canActivate: [EventGuard, LanguageGuard] },
{ path: 'update', component: UpdateTicketComponent, canActivate: [EventGuard, LanguageGuard] }
]},
{ path: 'my-orders', component: MyOrdersComponent, canActivate: [RemoveEventCssGuard, LanguageGuard] }
{ path: 'my-orders', component: MyOrdersComponent, canActivate: [UserLoggedInGuard, RemoveEventCssGuard, LanguageGuard] },
{ path: 'my-profile', component: MyProfileComponent, canActivate: [UserLoggedInGuard, RemoveEventCssGuard, LanguageGuard] }
];

@NgModule({
Expand Down
4 changes: 3 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import {PurchaseContextContainerComponent} from './purchase-context-container/pu
import { ModalRemoveSubscriptionComponent } from './reservation/modal-remove-subscription/modal-remove-subscription.component';
import {UserService} from './shared/user.service';
import {MyOrdersComponent} from './my-orders/my-orders.component';
import {MyProfileComponent} from './my-profile/my-profile.component';



Expand Down Expand Up @@ -141,7 +142,8 @@ export function InitUserService(userService: UserService): () => Promise<boolean
SubscriptionSummaryComponent,
PurchaseContextContainerComponent,
ModalRemoveSubscriptionComponent,
MyOrdersComponent
MyOrdersComponent,
MyProfileComponent
],
imports: [
BrowserModule,
Expand Down
2 changes: 1 addition & 1 deletion src/app/event-summary/event-summary.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class EventSummaryComponent {
constructor(public translate: TranslateService) { }

get isEventOnline(): boolean {
return this.event.format == 'ONLINE';
return this.event.format === 'ONLINE';
}

}
6 changes: 5 additions & 1 deletion src/app/event.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export class EventGuard implements CanActivate {
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
const eventShortName = next.params['eventShortName'];
return this.eventService.getEvent(eventShortName)
.pipe(tap((e) => handleCustomCss(e)), catchError(e => of(this.router.parseUrl(''))), map(e => e instanceof UrlTree ? e : true));
.pipe(
tap((e) => handleCustomCss(e)),
catchError(e => of(this.router.parseUrl(''))),
map(e => e instanceof UrlTree ? e : true)
);
}
}
3 changes: 2 additions & 1 deletion src/app/model/info.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AnalyticsConfiguration } from './analytics-configuration';
import {TermsPrivacyLinksContainer} from './event';
import {InvoicingConfiguration, TermsPrivacyLinksContainer} from './event';

export class Info {
demoModeEnabled: boolean;
Expand All @@ -8,6 +8,7 @@ export class Info {
analyticsConfiguration: AnalyticsConfiguration;
globalPrivacyPolicyUrl?: string;
globalTermsUrl?: string;
invoicingConfiguration?: InvoicingConfiguration;
}

export function globalTermsPrivacyLinks(info: Info): TermsPrivacyLinksContainer {
Expand Down
3 changes: 1 addition & 2 deletions src/app/model/reservation-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,7 @@ export interface BillingDetails {
}

export interface TicketReservationInvoicingAdditionalInfo {
italianEInvoicing: ItalianEInvoicing;

italianEInvoicing?: ItalianEInvoicing;
}

export interface ItalianEInvoicing {
Expand Down
42 changes: 42 additions & 0 deletions src/app/my-profile/my-profile.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<div class="container mt-2" *ngIf="userForm">
<form [formGroup]="userForm" (submit)="save()">
<div class="application-container p-md-4">
<app-topbar [contentLanguages]="languages"></app-topbar>
<div class="page-header">
<h1>{{ 'user.menu.my-profile' | translate }}</h1>
<small>{{ 'my-profile.description' | translate }}</small>
</div>

<div class="row mt-3" [formGroup]="userForm">
<div class="col-12 col-sm-6">
<div class="form-group">
<label for="first-name">{{'common.first-name'|translate}}{{' '}}*</label>
<input id="first-name" class="form-control" formControlName="firstName" aria-required="true" type="text" autocomplete="fname" [attr.maxlength]="255" appInvalidFeedback>
</div>
</div>
<div class="col-12 col-sm-6">
<div class="form-group">
<label for="last-name">{{'common.last-name'|translate}}{{' '}}*</label>
<input id="last-name" class="form-control" formControlName="lastName" aria-required="true" type="text" autocomplete="lname" [attr.maxlength]="255" appInvalidFeedback>
</div>
</div>
</div>

<div *ngIf="invoicingConfiguration.invoiceAllowed">
<app-invoice-form [form]="userForm" [invoicingConfiguration]="invoicingConfiguration"></app-invoice-form>
</div>

<hr>
<div class="mt-5">
<div class="row d-flex justify-content-md-between">
<div class="col-md-5 order-md-1 col-12 mb-2">
<button class="block-button btn btn-success">{{ 'common.confirm' | translate }}</button>
</div>
<div class="col-md-5 order-md-0 col-12 mt-2 mt-md-0 mb-2">
<a class="block-button btn btn-light" [routerLink]="['/']" translate="to-home"></a>
</div>
</div>
</div>
</div>
</form>
</div>
98 changes: 98 additions & 0 deletions src/app/my-profile/my-profile.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {Component, OnInit} from '@angular/core';
import {UserService} from '../shared/user.service';
import {User} from '../model/user';
import {TranslateService} from '@ngx-translate/core';
import {I18nService} from '../shared/i18n.service';
import {InvoicingConfiguration, Language} from '../model/event';
import {zip} from 'rxjs';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {BookingComponent} from '../reservation/booking/booking.component';
import {handleServerSideValidationError} from '../shared/validation-helper';
import {ErrorDescriptor} from '../model/validated-response';
import {InfoService} from '../shared/info.service';

@Component({
selector: 'app-my-profile',
templateUrl: './my-profile.component.html'
})
export class MyProfileComponent implements OnInit {

user?: User;
languages: Language[];
userForm?: FormGroup;
invoicingConfiguration?: InvoicingConfiguration;
globalErrors: ErrorDescriptor[];

constructor(private userService: UserService,
private translateService: TranslateService,
private i18nService: I18nService,
private formBuilder: FormBuilder,
private infoService: InfoService) {
this.userForm = this.formBuilder.group({
firstName: this.formBuilder.control(null, Validators.required),
lastName: this.formBuilder.control(null, Validators.required),
addCompanyBillingDetails: this.formBuilder.control(false),
billingAddressCompany: this.formBuilder.control(null),
billingAddressLine1: this.formBuilder.control(null),
billingAddressLine2: this.formBuilder.control(null),
billingAddressZip: this.formBuilder.control(null),
billingAddressCity: this.formBuilder.control(null),
billingAddressState: this.formBuilder.control(null),
vatCountryCode: this.formBuilder.control(null),
skipVatNr: this.formBuilder.control(false),
vatNr: this.formBuilder.control(null),
italyEInvoicingFiscalCode: this.formBuilder.control(null),
italyEInvoicingReferenceType: this.formBuilder.control(null),
italyEInvoicingReferenceAddresseeCode: this.formBuilder.control(null),
italyEInvoicingReferencePEC: this.formBuilder.control(null),
italyEInvoicingSplitPayment: this.formBuilder.control(null),
});
}

ngOnInit(): void {
zip(this.userService.getUserIdentity(), this.i18nService.getAvailableLanguages(), this.infoService.getInfo())
.subscribe(([user, languages, info]) => {
this.languages = languages;
this.i18nService.setPageTitle('user.menu.my-profile', null);
this.invoicingConfiguration = info.invoicingConfiguration;
let values: {[p: string]: any} = {
firstName: user.firstName,
lastName: user.lastName
};
const userBillingDetails = user.profile?.billingDetails;
if (userBillingDetails != null) {
values = {
...values,
billingAddressCompany: userBillingDetails.companyName,
billingAddressLine1: userBillingDetails.addressLine1,
billingAddressLine2: userBillingDetails.addressLine2,
billingAddressZip: userBillingDetails.zip,
billingAddressCity: userBillingDetails.city,
billingAddressState: userBillingDetails.state,
vatCountryCode: userBillingDetails.country,
vatNr: userBillingDetails.taxId,
italyEInvoicingFiscalCode: BookingComponent.optionalGet(userBillingDetails, (i) => i.fiscalCode),
italyEInvoicingReferenceType: BookingComponent.optionalGet(userBillingDetails, (i) => i.referenceType),
italyEInvoicingReferenceAddresseeCode: BookingComponent.optionalGet(userBillingDetails, (i) => i.addresseeCode),
italyEInvoicingReferencePEC: BookingComponent.optionalGet(userBillingDetails, (i) => i.pec),
italyEInvoicingSplitPayment: BookingComponent.optionalGet(userBillingDetails, (i) => i.splitPayment)
};
}
this.userForm.patchValue(values);
});
}


save(): void {
if (this.userForm.valid) {
this.userService.updateUser(this.userForm.value)
.subscribe(res => {
if (res.success) {
this.user = res.value;
} else {
handleServerSideValidationError(res.validationErrors, this.userForm);
}
}, err => this.globalErrors = handleServerSideValidationError(err, this.userForm));
}
}
}
2 changes: 1 addition & 1 deletion src/app/reservation/booking/booking.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class BookingComponent implements OnInit, AfterViewInit {
enableAttendeeAutocomplete: boolean;
displayLoginSuggestion: boolean;

private static optionalGet<T>(billingDetails: BillingDetails, consumer: (b: ItalianEInvoicing) => T, userBillingDetails?: BillingDetails): T | null {
public static optionalGet<T>(billingDetails: BillingDetails, consumer: (b: ItalianEInvoicing) => T, userBillingDetails?: BillingDetails): T | null {
const italianEInvoicing = billingDetails.invoicingAdditionalInfo.italianEInvoicing;
if (italianEInvoicing != null) {
return consumer(italianEInvoicing);
Expand Down
20 changes: 14 additions & 6 deletions src/app/reservation/invoice-form/invoice-form.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { I18nService } from 'src/app/shared/i18n.service';
import { Subscription } from 'rxjs';
import { LocalizedCountry } from 'src/app/model/localized-country';
import { PurchaseContext } from 'src/app/model/purchase-context';
import {InvoicingConfiguration} from '../../model/event';

@Component({
selector: 'app-invoice-form',
Expand All @@ -16,7 +17,10 @@ export class InvoiceFormComponent implements OnInit, OnDestroy {
form: FormGroup;

@Input()
purchaseContext: PurchaseContext;
purchaseContext?: PurchaseContext;

@Input()
invoicingConfiguration?: InvoicingConfiguration;

private langChangeSub: Subscription;

Expand All @@ -37,7 +41,7 @@ export class InvoiceFormComponent implements OnInit, OnDestroy {
this.form.get('italyEInvoicingReferenceType').valueChanges.subscribe(change => {
this.updateItalyEInvoicingFields();
});
this.form.get('skipVatNr').valueChanges.subscribe(change => {
this.form.get('skipVatNr')?.valueChanges.subscribe(change => {
this.taxIdIsRequired = !change;
});
}
Expand Down Expand Up @@ -76,23 +80,23 @@ export class InvoiceFormComponent implements OnInit, OnDestroy {
}

get euVatCheckingEnabled(): boolean {
return this.purchaseContext.invoicingConfiguration.euVatCheckingEnabled;
return this.invoicingConf.euVatCheckingEnabled;
}

get customerReferenceEnabled(): boolean {
return this.purchaseContext.invoicingConfiguration.customerReferenceEnabled;
return this.invoicingConf.customerReferenceEnabled;
}

get invoiceBusiness(): boolean {
return this.form.value.addCompanyBillingDetails;
}

get vatNumberStrictlyRequired(): boolean {
return this.purchaseContext.invoicingConfiguration.vatNumberStrictlyRequired;
return this.invoicingConf.vatNumberStrictlyRequired;
}

get enabledItalyEInvoicing(): boolean {
return this.purchaseContext.invoicingConfiguration.enabledItalyEInvoicing;
return this.invoicingConf.enabledItalyEInvoicing;
}

get italyEInvoicingFormDisplayed(): boolean {
Expand All @@ -103,6 +107,10 @@ export class InvoiceFormComponent implements OnInit, OnDestroy {
return this.form.value.vatCountryCode != null;
}

private get invoicingConf(): InvoicingConfiguration {
return this.purchaseContext?.invoicingConfiguration || this.invoicingConfiguration;
}

searchCountry(term: string, country: LocalizedCountry): boolean {
if (term) {
term = term.toLowerCase();
Expand Down
29 changes: 24 additions & 5 deletions src/app/shared/language-selector/language-selector.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core';
import {Component, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core';
import { Language } from '../../model/event';
import { Router } from '@angular/router';
import { I18nService } from '../../shared/i18n.service';
Expand All @@ -7,7 +7,7 @@ import { I18nService } from '../../shared/i18n.service';
selector: 'app-language-selector',
templateUrl: './language-selector.component.html'
})
export class LanguageSelectorComponent implements OnInit {
export class LanguageSelectorComponent implements OnInit, OnChanges {

@Input()
private contentLanguages: Language[];
Expand All @@ -22,13 +22,32 @@ export class LanguageSelectorComponent implements OnInit {
this.buildValues();
}

ngOnChanges(changes: SimpleChanges): void {
if (changes.contentLanguages) {
this.buildValues();
}
}



private buildValues(): void {
const currentLang = this.contentLanguages.find(cl => cl.locale === this.selectedLanguage);
this.currentLanguage = currentLang ? currentLang.displayLanguage : null;
this.filteredLanguages = this.contentLanguages.filter(cl => cl !== currentLang).sort((a, b) => a.displayLanguage.toLowerCase() > b.displayLanguage.toLowerCase() ? 1 : -1);
if (this.contentLanguages != null && this.contentLanguages.length > 0) {
let currentLang = this.contentLanguages.find(cl => cl.locale === this.selectedLanguage);
const languageNotFound = currentLang == null;
if (languageNotFound) {
currentLang = this.contentLanguages[0];
this.changeLanguage(currentLang.locale);
}
this.currentLanguage = currentLang ? currentLang.displayLanguage : this.contentLanguages[0].displayLanguage;
this.filteredLanguages = this.contentLanguages.filter(cl => cl !== currentLang)
.sort((a, b) => a.displayLanguage.toLowerCase() > b.displayLanguage.toLowerCase() ? 1 : -1);
}
}

public changeLanguage(lang: string): void {
if (this.selectedLanguage === lang) {
return;
}
const eventShortName = this.router.routerState.snapshot.root.firstChild ? this.router.routerState.snapshot.root.firstChild.params['eventShortName'] : null;
this.i18nService.useTranslation('event', eventShortName, lang).subscribe(() => {
this.selectedLanguage = this.i18nService.getCurrentLang();
Expand Down
1 change: 1 addition & 0 deletions src/app/shared/topbar/top-bar.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<fa-icon [icon]="['fas', 'user-astronaut']" aria-hidden="true" [title]="'user.menu.title' | translate"></fa-icon> {{user?.firstName}} {{user?.lastName}}
</button>
<div ngbDropdownMenu aria-labelledby="userDropdown">
<button ngbDropdownItem class="btn btn-link" (click)="myProfile()"><fa-icon [icon]="['fas', 'user-astronaut']" a11yRole="presentation"></fa-icon> <span class="ml-3">{{ 'user.menu.my-profile' | translate }}</span></button>
<button ngbDropdownItem class="btn btn-link" (click)="myOrders()"><fa-icon [icon]="['fas', 'ticket-alt']" [classes]="['rotate-45']" a11yRole="presentation"></fa-icon> <span class="ml-3">{{ 'user.menu.my-orders' | translate }}</span></button>
<button ngbDropdownItem class="btn btn-link" (click)="logout()"><fa-icon [icon]="['fas', 'sign-out-alt']" a11yRole="presentation"></fa-icon> <span class="ml-3">{{ 'user.menu.logout' | translate }}</span></button>
</div>
Expand Down
4 changes: 4 additions & 0 deletions src/app/shared/topbar/top-bar.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@ export class TopBarComponent implements OnInit, OnDestroy {
myOrders(): void {
this.router.navigate(['my-orders']);
}

myProfile(): void {
this.router.navigate(['my-profile']);
}
}
7 changes: 6 additions & 1 deletion src/app/shared/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {HttpClient} from '@angular/common/http';
import {ANONYMOUS, AuthenticationStatus, PurchaseContextWithReservation, User} from '../model/user';
import {BehaviorSubject, interval, Observable, of, Subject, Subscription, timer} from 'rxjs';
import {map, mergeMap, tap, timeout} from 'rxjs/operators';
import {ValidatedResponse} from '../model/validated-response';

@Injectable({ providedIn: 'root' })
export class UserService implements OnDestroy {
Expand Down Expand Up @@ -37,7 +38,7 @@ export class UserService implements OnDestroy {
);
}

private getUserIdentity(): Observable<User> {
public getUserIdentity(): Observable<User> {
return this.http.get<User>('/api/v2/public/user/me', { observe: 'response' })
.pipe(map(response => {
if (response.status === 204) {
Expand All @@ -64,6 +65,10 @@ export class UserService implements OnDestroy {
return this.http.get<Array<PurchaseContextWithReservation>>('/api/v2/public/user/reservations');
}

updateUser(user: any): Observable<ValidatedResponse<User>> {
return this.http.post<ValidatedResponse<User>>('/api/v2/public/user/me', user);
}

private startPolling(): void {
this.authStatusSubscription = interval(30_000).pipe(
mergeMap(() => this.loadUserStatus())
Expand Down
Loading

0 comments on commit b0c4268

Please sign in to comment.