diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 578056d..3fd5e87 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -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'}; @@ -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({ diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2a6b5fc..04f922b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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'; @@ -141,7 +142,8 @@ export function InitUserService(userService: UserService): () => Promise { 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) + ); } } diff --git a/src/app/model/info.ts b/src/app/model/info.ts index 44817bc..a9dd94c 100644 --- a/src/app/model/info.ts +++ b/src/app/model/info.ts @@ -1,5 +1,5 @@ import { AnalyticsConfiguration } from './analytics-configuration'; -import {TermsPrivacyLinksContainer} from './event'; +import {InvoicingConfiguration, TermsPrivacyLinksContainer} from './event'; export class Info { demoModeEnabled: boolean; @@ -8,6 +8,7 @@ export class Info { analyticsConfiguration: AnalyticsConfiguration; globalPrivacyPolicyUrl?: string; globalTermsUrl?: string; + invoicingConfiguration?: InvoicingConfiguration; } export function globalTermsPrivacyLinks(info: Info): TermsPrivacyLinksContainer { diff --git a/src/app/model/reservation-info.ts b/src/app/model/reservation-info.ts index 011e66c..29a0740 100644 --- a/src/app/model/reservation-info.ts +++ b/src/app/model/reservation-info.ts @@ -114,8 +114,7 @@ export interface BillingDetails { } export interface TicketReservationInvoicingAdditionalInfo { - italianEInvoicing: ItalianEInvoicing; - + italianEInvoicing?: ItalianEInvoicing; } export interface ItalianEInvoicing { diff --git a/src/app/my-profile/my-profile.component.html b/src/app/my-profile/my-profile.component.html new file mode 100644 index 0000000..a157a2c --- /dev/null +++ b/src/app/my-profile/my-profile.component.html @@ -0,0 +1,42 @@ +
+
+
+ + + +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ +
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+
diff --git a/src/app/my-profile/my-profile.component.ts b/src/app/my-profile/my-profile.component.ts new file mode 100644 index 0000000..c9f7e00 --- /dev/null +++ b/src/app/my-profile/my-profile.component.ts @@ -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)); + } + } +} diff --git a/src/app/reservation/booking/booking.component.ts b/src/app/reservation/booking/booking.component.ts index 2073111..5719e97 100644 --- a/src/app/reservation/booking/booking.component.ts +++ b/src/app/reservation/booking/booking.component.ts @@ -46,7 +46,7 @@ export class BookingComponent implements OnInit, AfterViewInit { enableAttendeeAutocomplete: boolean; displayLoginSuggestion: boolean; - private static optionalGet(billingDetails: BillingDetails, consumer: (b: ItalianEInvoicing) => T, userBillingDetails?: BillingDetails): T | null { + public static optionalGet(billingDetails: BillingDetails, consumer: (b: ItalianEInvoicing) => T, userBillingDetails?: BillingDetails): T | null { const italianEInvoicing = billingDetails.invoicingAdditionalInfo.italianEInvoicing; if (italianEInvoicing != null) { return consumer(italianEInvoicing); diff --git a/src/app/reservation/invoice-form/invoice-form.component.ts b/src/app/reservation/invoice-form/invoice-form.component.ts index 9f083cb..22bca93 100644 --- a/src/app/reservation/invoice-form/invoice-form.component.ts +++ b/src/app/reservation/invoice-form/invoice-form.component.ts @@ -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', @@ -16,7 +17,10 @@ export class InvoiceFormComponent implements OnInit, OnDestroy { form: FormGroup; @Input() - purchaseContext: PurchaseContext; + purchaseContext?: PurchaseContext; + + @Input() + invoicingConfiguration?: InvoicingConfiguration; private langChangeSub: Subscription; @@ -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; }); } @@ -76,11 +80,11 @@ 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 { @@ -88,11 +92,11 @@ export class InvoiceFormComponent implements OnInit, OnDestroy { } 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 { @@ -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(); diff --git a/src/app/shared/language-selector/language-selector.component.ts b/src/app/shared/language-selector/language-selector.component.ts index c3ac0ae..15f27e9 100644 --- a/src/app/shared/language-selector/language-selector.component.ts +++ b/src/app/shared/language-selector/language-selector.component.ts @@ -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'; @@ -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[]; @@ -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(); diff --git a/src/app/shared/topbar/top-bar.component.html b/src/app/shared/topbar/top-bar.component.html index f69ad08..aa4092f 100644 --- a/src/app/shared/topbar/top-bar.component.html +++ b/src/app/shared/topbar/top-bar.component.html @@ -11,6 +11,7 @@ {{user?.firstName}} {{user?.lastName}}
+
diff --git a/src/app/shared/topbar/top-bar.component.ts b/src/app/shared/topbar/top-bar.component.ts index a44d159..049b9b3 100644 --- a/src/app/shared/topbar/top-bar.component.ts +++ b/src/app/shared/topbar/top-bar.component.ts @@ -50,4 +50,8 @@ export class TopBarComponent implements OnInit, OnDestroy { myOrders(): void { this.router.navigate(['my-orders']); } + + myProfile(): void { + this.router.navigate(['my-profile']); + } } diff --git a/src/app/shared/user.service.ts b/src/app/shared/user.service.ts index fd9a05c..64b2afd 100644 --- a/src/app/shared/user.service.ts +++ b/src/app/shared/user.service.ts @@ -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 { @@ -37,7 +38,7 @@ export class UserService implements OnDestroy { ); } - private getUserIdentity(): Observable { + public getUserIdentity(): Observable { return this.http.get('/api/v2/public/user/me', { observe: 'response' }) .pipe(map(response => { if (response.status === 204) { @@ -64,6 +65,10 @@ export class UserService implements OnDestroy { return this.http.get>('/api/v2/public/user/reservations'); } + updateUser(user: any): Observable> { + return this.http.post>('/api/v2/public/user/me', user); + } + private startPolling(): void { this.authStatusSubscription = interval(30_000).pipe( mergeMap(() => this.loadUserStatus()) diff --git a/src/app/user-logged-in.guard.ts b/src/app/user-logged-in.guard.ts new file mode 100644 index 0000000..d0e0a16 --- /dev/null +++ b/src/app/user-logged-in.guard.ts @@ -0,0 +1,28 @@ +import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree} from '@angular/router'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; +import {UserService} from './shared/user.service'; +import {first, map} from 'rxjs/operators'; +import {ANONYMOUS} from './model/user'; + +@Injectable({ + providedIn: 'root' +}) +export class UserLoggedInGuard implements CanActivate { + + constructor(private userService: UserService, private router: Router) {} + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + this.router.parseUrl(''); + return this.userService.authenticationStatus + .pipe( + first(), + map(status => { + if (status.enabled && status.user !== ANONYMOUS) { + return true; + } + return this.router.parseUrl(''); + }) + ); + } +}