-
-
-

- Create Organization -

+
+

+ Create Organization +

+
+
+ +
+
-
- - @switch (currentStep().key) { - @case ('basics') { - - } +
+ +
- @case ('localization') { -
- Localization content -
- } +
+ +
- @case ('plan-limits') { -
- Plan and Limits content -
- } +
+ +
+
+
- @case ('admin-user') { -
- Admin and User content -
- } - } -
-
-
-
\ No newline at end of file + + + + + + + diff --git a/src/app/features/organizations/organization-onboarding/organization-onboarding.ts b/src/app/features/organizations/organization-onboarding/organization-onboarding.ts index 5d18da07..3d93e7bf 100644 --- a/src/app/features/organizations/organization-onboarding/organization-onboarding.ts +++ b/src/app/features/organizations/organization-onboarding/organization-onboarding.ts @@ -4,7 +4,8 @@ import { computed, DestroyRef, inject, - signal + signal, + viewChild, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Router } from '@angular/router'; @@ -15,13 +16,26 @@ import { OnboardingStepper } from './components/onboarding-stepper/onboarding-stepper'; import { OrganizationBasicsStepComponent } from './steps/organization-basics/organization-basics'; +import { OrganizationLocalizationStepComponent } from './steps/organization-localization/organization-localization'; +import { OrganizationPlanLimitsStepComponent } from './steps/organization-plan-limits/organization-plan-limits'; +import { OrganizationAdminStepComponent } from './steps/organization-admin/organization-admin'; import { OrganizationOnboardingStateService } from './services/organization-onboarding-state.service'; import { OrganizationOnboardingService } from './services/organization-onboarding.service'; +import { ConfirmDialog } from '../../../shared/components/confirm-dialog/confirm-dialog'; +import { OnboardingStepForm } from './models/organization-onboarding.model'; +import { OrganizationProvisioningRequest } from './models/organization-provisioning.model'; @Component({ selector: 'app-organization-onboarding', standalone: true, - imports: [OnboardingStepper, OrganizationBasicsStepComponent], + imports: [ + OnboardingStepper, + OrganizationBasicsStepComponent, + OrganizationLocalizationStepComponent, + OrganizationPlanLimitsStepComponent, + OrganizationAdminStepComponent, + ConfirmDialog, + ], templateUrl: './organization-onboarding.html', styleUrl: './organization-onboarding.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -31,7 +45,7 @@ export class OrganizationOnboarding { private readonly destroyRef = inject(DestroyRef); private readonly router = inject(Router); private readonly toastr = inject(ToastrService); - private readonly stateService = inject(OrganizationOnboardingStateService); + readonly stateService = inject(OrganizationOnboardingStateService); private readonly onboardingService = inject(OrganizationOnboardingService); readonly steps: readonly OnboardingStep[] = this.stateService.createStepDefinitions(); @@ -39,7 +53,15 @@ export class OrganizationOnboarding { readonly savingDraft = signal(false); readonly finishing = signal(false); - private basicsStep?: OrganizationBasicsStepComponent; + private readonly basicsStep = viewChild(OrganizationBasicsStepComponent); + private readonly localizationStep = viewChild(OrganizationLocalizationStepComponent); + private readonly planLimitsStep = viewChild(OrganizationPlanLimitsStepComponent); + private readonly adminStep = viewChild(OrganizationAdminStepComponent); + private readonly cancelConfirmDialog = viewChild('cancelConfirmDialog'); + private readonly finishConfirmDialog = viewChild('finishConfirmDialog'); + private savedDraftSnapshot = ''; + + readonly provisioningRequest = signal(null); readonly currentStepIndex = this.stateService.currentStepIndex; readonly completedStepIndexes = this.stateService.completedStepIndexes; @@ -63,28 +85,24 @@ export class OrganizationOnboarding { ) .subscribe(draft => { if (!draft) { + this.savedDraftSnapshot = this.serializeDraftState(); return; } this.stateService.restoreDraft(draft); + this.savedDraftSnapshot = this.serializeDraftState(); + this.hydrateActiveStep(); }); } - registerBasicsStep(component: OrganizationBasicsStepComponent): void { - this.basicsStep = component; - const basicsDraft = this.stateService.onboardingDraft().basics; - - if (basicsDraft) { - component.patchValue(basicsDraft); - } - } - onStepSelected(index: number): void { if (!this.stateService.canOpenStep(index)) { return; } + this.captureActiveStepDraft(); this.stateService.setCurrentStepIndex(index); + this.hydrateActiveStep(); } onBack(): void { @@ -94,7 +112,9 @@ export class OrganizationOnboarding { return; } + this.captureActiveStepDraft(); this.stateService.setCurrentStepIndex(currentIndex - 1); + this.hydrateActiveStep(); } onNext(): void { @@ -104,17 +124,16 @@ export class OrganizationOnboarding { return; } - if (currentIndex === 0) { - if (!this.basicsStep?.validate()) { - this.stateService.unmarkStepCompleted(currentIndex); - return; - } - - this.stateService.updateBasics(this.basicsStep.getValue()); + const activeStep = this.getActiveStep(); + if (!activeStep?.validate()) { + this.stateService.unmarkStepCompleted(currentIndex); + return; } + this.storeValidatedActiveStep(activeStep); this.stateService.markStepCompleted(currentIndex); this.stateService.setCurrentStepIndex(currentIndex + 1); + this.hydrateActiveStep(); } onSaveDraft(): void { @@ -122,9 +141,7 @@ export class OrganizationOnboarding { return; } - if (this.currentStepIndex() === 0 && this.basicsStep) { - this.stateService.updateBasicsDraft(this.basicsStep.getDraftValue()); - } + this.captureActiveStepDraft(); this.savingDraft.set(true); @@ -137,6 +154,7 @@ export class OrganizationOnboarding { ) .subscribe({ next: () => { + this.savedDraftSnapshot = this.serializeDraftState(); this.toastr.success('Onboarding draft saved successfully.', 'Draft saved'); }, error: () => { @@ -150,15 +168,182 @@ export class OrganizationOnboarding { return; } - this.finishing.set(true); + const activeStep = this.adminStep(); + if (!activeStep?.validate()) { + this.stateService.unmarkStepCompleted(3); + return; + } - queueMicrotask(() => { - this.finishing.set(false); + this.stateService.updateAdmin(activeStep.getValue()); + this.stateService.markStepCompleted(3); + + const data = this.stateService.onboardingData(); + if (!data.basics || !data.localization || !data.planLimits || !data.admin) { + this.toastr.error('Complete and validate every onboarding step before provisioning.'); + return; + } + + this.provisioningRequest.set({ + basics: data.basics, + localization: data.localization, + planLimits: data.planLimits, + admin: data.admin, }); + void this.finishConfirmDialog()?.open(); } onCancel(): void { - this.stateService.clear(); - void this.router.navigate(['/configuration/organizations']); + this.captureActiveStepDraft(); + if (this.serializeDraftState() !== this.savedDraftSnapshot) { + void this.cancelConfirmDialog()?.open(); + return; + } + + void this.leaveOnboarding(false); } -} \ No newline at end of file + + onDiscardAndLeave(): void { + void this.leaveOnboarding(true); + } + + onProvisioningConfirmed(): void { + this.toastr.info( + 'The provisioning request is ready. Backend provisioning is not connected yet.', + 'Provisioning pending' + ); + } + + private getActiveStep(): OnboardingStepForm | undefined { + switch (this.currentStepIndex()) { + case 0: + return this.basicsStep(); + case 1: + return this.localizationStep(); + case 2: + return this.planLimitsStep(); + case 3: + return this.adminStep(); + default: + return undefined; + } + } + + private storeValidatedActiveStep(step: OnboardingStepForm): void { + switch (this.currentStepIndex()) { + case 0: + this.stateService.updateBasics((step as OrganizationBasicsStepComponent).getValue()); + break; + case 1: + this.stateService.updateLocalization((step as OrganizationLocalizationStepComponent).getValue()); + break; + case 2: + this.stateService.updatePlanLimits((step as OrganizationPlanLimitsStepComponent).getValue()); + break; + case 3: + this.stateService.updateAdmin((step as OrganizationAdminStepComponent).getValue()); + break; + } + } + + private captureActiveStepDraft(): void { + const currentIndex = this.currentStepIndex(); + const validatedValue = this.getValidatedStepValue(currentIndex); + + switch (currentIndex) { + case 0: + this.stateService.updateBasicsDraft(this.basicsStep()?.getDraftValue() ?? null); + break; + case 1: + this.stateService.updateLocalizationDraft(this.localizationStep()?.getDraftValue() ?? null); + break; + case 2: + this.stateService.updatePlanLimitsDraft(this.planLimitsStep()?.getDraftValue() ?? null); + break; + case 3: + this.stateService.updateAdminDraft(this.adminStep()?.getDraftValue() ?? null); + break; + } + + const draftValue = this.getDraftStepValue(currentIndex); + if ( + this.stateService.isStepCompleted(currentIndex) && + JSON.stringify(draftValue) !== JSON.stringify(validatedValue) + ) { + this.stateService.invalidateStep(currentIndex); + } + } + + private getValidatedStepValue(index: number): unknown { + const data = this.stateService.onboardingData(); + return [data.basics, data.localization, data.planLimits, data.admin][index] ?? null; + } + + private getDraftStepValue(index: number): unknown { + const draft = this.stateService.onboardingDraft(); + return [draft.basics, draft.localization, draft.planLimits, draft.admin][index] ?? null; + } + + private hydrateActiveStep(): void { + queueMicrotask(() => { + const draft = this.stateService.onboardingDraft(); + switch (this.currentStepIndex()) { + case 0: + if (draft.basics) this.basicsStep()?.patchValue(draft.basics); + break; + case 1: + if (draft.localization) { + this.localizationStep()?.patchValue(draft.localization); + } + this.applyCountryDefaults(); + break; + case 2: + if (draft.planLimits) this.planLimitsStep()?.patchValue(draft.planLimits); + break; + case 3: + if (draft.admin) this.adminStep()?.patchValue(draft.admin); + break; + } + }); + } + + private applyCountryDefaults(): void { + const countryIso2 = this.stateService.basics()?.registrationCountry.iso2 ?? null; + this.onboardingService.getCountryLocalizationDefaults(countryIso2) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(defaults => { + if (defaults) { + this.localizationStep()?.applyCountryDefaults(defaults, false); + } + }); + } + + private serializeDraftState(): string { + const draft = this.stateService.buildDraft(); + return JSON.stringify({ + currentStepIndex: draft.currentStepIndex, + completedStepIndexes: draft.completedStepIndexes, + basics: draft.basics, + localization: draft.localization, + planLimits: draft.planLimits, + admin: draft.admin, + }); + } + + private async leaveOnboarding(clearDraft: boolean): Promise { + if (clearDraft) { + try { + await new Promise((resolve, reject) => { + this.onboardingService.clearDraft() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ next: resolve, error: reject }); + }); + } catch { + this.toastr.error('Unable to clear the saved draft.'); + return; + } + } + + this.stateService.clear(); + await this.router.navigate(['/organizations']); + } +} diff --git a/src/app/features/organizations/organization-onboarding/services/organization-onboarding-state.service.ts b/src/app/features/organizations/organization-onboarding/services/organization-onboarding-state.service.ts index 7ae2e8a6..92f47f5e 100644 --- a/src/app/features/organizations/organization-onboarding/services/organization-onboarding-state.service.ts +++ b/src/app/features/organizations/organization-onboarding/services/organization-onboarding-state.service.ts @@ -57,7 +57,7 @@ export class OrganizationOnboardingStateService { }); setCurrentStepIndex(index: number): void { - this.currentStepIndexState.set(Math.max(0, index)); + this.currentStepIndexState.set(Math.min(3, Math.max(0, index))); } updateBasics(value: OrganizationBasicsValue | null): void { @@ -108,8 +108,27 @@ export class OrganizationOnboardingStateService { this.completedStepIndexesState.update(current => current.filter(item => item !== index)); } + invalidateStep(index: number): void { + this.unmarkStepCompleted(index); + this.onboardingDataState.update(current => { + switch (index) { + case 0: + return { ...current, basics: null }; + case 1: + return { ...current, localization: null }; + case 2: + return { ...current, planLimits: null }; + case 3: + return { ...current, admin: null }; + default: + return current; + } + }); + } + setCompletedStepIndexes(indexes: readonly number[]): void { - const uniqueIndexes = [...new Set(indexes.filter(index => index >= 0))].sort((left, right) => left - right); + const uniqueIndexes = [...new Set(indexes.filter(index => index >= 0 && index <= 3))] + .sort((left, right) => left - right); this.completedStepIndexesState.set(uniqueIndexes); } @@ -158,7 +177,7 @@ export class OrganizationOnboardingStateService { ? { ...draft.admin } as OrganizationAdminValue : null, }); - this.currentStepIndexState.set(Math.max(0, draft.currentStepIndex)); + this.currentStepIndexState.set(Math.min(3, Math.max(0, draft.currentStepIndex))); this.setCompletedStepIndexes(draft.completedStepIndexes); this.savedDraftIdState.set(draft.draftId); this.savedAtState.set(draft.savedAt); @@ -180,10 +199,10 @@ export class OrganizationOnboardingStateService { createStepDefinitions(): readonly OnboardingStepDefinition[] { return [ - { key: 'basics', label: 'Basics' }, + { key: 'basics', label: 'Basic' }, { key: 'localization', label: 'Localization' }, - { key: 'plan-limits', label: 'Plan & Limits' }, - { key: 'admin-user', label: 'Admin & User' }, + { key: 'plan-limits', label: 'Plans and Limits' }, + { key: 'admin-user', label: 'Admin & Users' }, ]; } -} \ No newline at end of file +} diff --git a/src/app/features/organizations/organization-onboarding/services/organization-onboarding.service.ts b/src/app/features/organizations/organization-onboarding/services/organization-onboarding.service.ts index 2f92c05a..013c802e 100644 --- a/src/app/features/organizations/organization-onboarding/services/organization-onboarding.service.ts +++ b/src/app/features/organizations/organization-onboarding/services/organization-onboarding.service.ts @@ -1,6 +1,6 @@ import { Injectable, inject } from '@angular/core'; -import { FormSelect } from '../../../../shared/components/form/form-select/form-select'; -import { Observable, catchError, map, of, throwError } from 'rxjs'; +import { Observable, map, of, throwError } from 'rxjs'; +import { FormSelectOption } from '../../../../shared/components/form/models/form-select.models'; import { CountryLookupDto, CountryService, @@ -29,10 +29,6 @@ import { OrganizationPlanLookupValue, TimeFormatValue, } from '../models/organization-onboarding.model'; -import { - OrganizationProvisioningRequest, - OrganizationProvisioningResult, -} from '../models/organization-provisioning.model'; const ORGANIZATION_ONBOARDING_DRAFT_KEY = 'organization-onboarding-draft-v1'; @@ -84,31 +80,31 @@ const PLAN_DEFAULTS: readonly OrganizationPlanDefaults[] = [ }, ]; -const DATE_FORMAT_OPTIONS: readonly FormSelect[] = [ - // { value: 'DD/MM/YYYY', label: 'DD/MM/YYYY' }, - // { value: 'MM/DD/YYYY', label: 'MM/DD/YYYY' }, - // { value: 'YYYY-MM-DD', label: 'YYYY-MM-DD' }, +const DATE_FORMAT_OPTIONS: readonly FormSelectOption[] = [ + { value: 'DD/MM/YYYY', label: 'DD/MM/YYYY' }, + { value: 'MM/DD/YYYY', label: 'MM/DD/YYYY' }, + { value: 'YYYY-MM-DD', label: 'YYYY-MM-DD' }, ]; -const TIME_FORMAT_OPTIONS: readonly FormSelect[] = [ - // { value: 'TwelveHour', label: '12 hour' }, - // { value: 'TwentyFourHour', label: '24 hour' }, +const TIME_FORMAT_OPTIONS: readonly FormSelectOption[] = [ + { value: 'TwelveHour', label: '12 hour' }, + { value: 'TwentyFourHour', label: '24 hour' }, ]; -const NUMBER_FORMAT_OPTIONS: readonly FormSelect[] = [ - // { value: 'OneTwoThreeCommaFourFiveSixPointSevenEight', label: '123,456.78' }, - // { value: 'OneTwoThreePointFourFiveSixCommaSevenEight', label: '123.456,78' }, +const NUMBER_FORMAT_OPTIONS: readonly FormSelectOption[] = [ + { value: 'OneTwoThreeCommaFourFiveSixPointSevenEight', label: '123,456.78' }, + { value: 'OneTwoThreePointFourFiveSixCommaSevenEight', label: '123.456,78' }, ]; -const FISCAL_YEAR_OPTIONS: readonly FormSelect[] = [ - // { value: 'CalendarYear', label: 'Calendar Year (Jan-Dec)' }, - // { value: 'AprilToMarch', label: 'April - March' }, - // { value: 'JulyToJune', label: 'July - June' }, +const FISCAL_YEAR_OPTIONS: readonly FormSelectOption[] = [ + { value: 'CalendarYear', label: 'Calendar Year (Jan-Dec)' }, + { value: 'AprilToMarch', label: 'April - March' }, + { value: 'JulyToJune', label: 'July - June' }, ]; -const LICENSE_TYPE_OPTIONS: readonly FormSelect[] = [ - // { value: 'Trial', label: 'Trial' }, - // { value: 'Paid', label: 'Paid' }, +const LICENSE_TYPE_OPTIONS: readonly FormSelectOption[] = [ + { value: 'Trial', label: 'Trial' }, + { value: 'Paid', label: 'Paid' }, ]; const LOCALIZATION_DEFAULTS: readonly CountryLocalizationDefaults[] = [ @@ -202,35 +198,32 @@ export class OrganizationOnboardingService { })) ); } -loadLanguageOptions(limit = 50):readonly FormSelect[] { - return DATE_FORMAT_OPTIONS; + loadLanguageOptions(limit = 50): Observable[]> { + return this.languageService.autocomplete('', limit).pipe( + map(items => items.map(item => ({ + value: item.id, + label: `${item.code} - ${item.name}`, + }))) + ); } - // loadLanguageOptions(limit = 50): Observable[]> { - // // return this.languageService.autocomplete('', limit).pipe( - // // map(items => items.map(item => ({ - // // value: item.id, - // // label: `${item.code} - ${item.name}`, - // // }))) - // // ); - // } - getDateFormatOptions(): readonly FormSelect[] { + getDateFormatOptions(): readonly FormSelectOption[] { return DATE_FORMAT_OPTIONS; } - getTimeFormatOptions(): readonly FormSelect[] { + getTimeFormatOptions(): readonly FormSelectOption[] { return TIME_FORMAT_OPTIONS; } - getNumberFormatOptions(): readonly FormSelect[] { + getNumberFormatOptions(): readonly FormSelectOption[] { return NUMBER_FORMAT_OPTIONS; } - getFiscalYearOptions(): readonly FormSelect[] { + getFiscalYearOptions(): readonly FormSelectOption[] { return FISCAL_YEAR_OPTIONS; } - getLicenseTypeOptions(): readonly FormSelect[] { + getLicenseTypeOptions(): readonly FormSelectOption[] { return LICENSE_TYPE_OPTIONS; } @@ -271,7 +264,11 @@ loadLanguageOptions(limit = 50):readonly FormSelect[] { return of(parsedDraft); } catch (error) { - sessionStorage.removeItem(ORGANIZATION_ONBOARDING_DRAFT_KEY); + try { + sessionStorage.removeItem(ORGANIZATION_ONBOARDING_DRAFT_KEY); + } catch { + // The original storage or parsing error is the useful error to report. + } return throwError(() => error); } } @@ -294,13 +291,6 @@ loadLanguageOptions(limit = 50):readonly FormSelect[] { } } - provisionOrganization(_request: OrganizationProvisioningRequest): Observable { - return of({ - status: 'not-configured', - message: 'Provisioning is not configured yet for organization onboarding.', - }); - } - private filterLookupValues( source: readonly TValue[], term: string | null, @@ -325,8 +315,21 @@ loadLanguageOptions(limit = 50):readonly FormSelect[] { return ( value.schemaVersion === 1 && typeof value.currentStepIndex === 'number' && + Number.isInteger(value.currentStepIndex) && + value.currentStepIndex >= 0 && + value.currentStepIndex <= 3 && Array.isArray(value.completedStepIndexes) && - typeof value.savedAt === 'string' + value.completedStepIndexes.every(index => Number.isInteger(index) && index >= 0 && index <= 3) && + typeof value.savedAt === 'string' && + (value.draftId === null || typeof value.draftId === 'string') && + this.isDraftSection(value.basics) && + this.isDraftSection(value.localization) && + this.isDraftSection(value.planLimits) && + this.isDraftSection(value.admin) ); } -} \ No newline at end of file + + private isDraftSection(value: unknown): boolean { + return value === null || (typeof value === 'object' && !Array.isArray(value)); + } +} diff --git a/src/app/features/organizations/organization-onboarding/steps/organization-admin/organization-admin.html b/src/app/features/organizations/organization-onboarding/steps/organization-admin/organization-admin.html new file mode 100644 index 00000000..9f0ca37b --- /dev/null +++ b/src/app/features/organizations/organization-onboarding/steps/organization-admin/organization-admin.html @@ -0,0 +1,156 @@ +
+
+
+

+ Organization Contact +

+
+ +
+ +
+ +
+ +
+ +
+

+ First Administrator +

+

+ A set-password link will be sent to this administrator. +

+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+

+ Final Review +

+ +
+
+
Organization
+
+ {{ reviewData().basics?.organizationName || 'Not completed' }} +
+
+
+
Country
+
+ {{ reviewData().basics?.registrationCountry?.label || 'Not completed' }} +
+
+
+
Localization
+
+ {{ reviewData().localization?.timeZone?.label || 'Not completed' }} · + {{ reviewData().localization?.currency?.secondaryLabel || reviewData().localization?.currency?.label || '—' }} +
+
+
+
Plan and limits
+
+ {{ reviewData().planLimits?.subscriptionPlan?.label || 'Not completed' }} + @if (reviewData().planLimits) { + · {{ reviewData().planLimits?.maximumCompanies }} companies · + {{ reviewData().planLimits?.maximumUsers }} users · + {{ reviewData().planLimits?.maximumStorageGb }} GB + } +
+
+
+
Access dates
+
+ {{ reviewData().planLimits?.systemAccessStartDate || 'Not completed' }} – + {{ reviewData().planLimits?.systemAccessEndDate || 'No end date' }} +
+
+
+
Administrator
+
+ {{ form.controls.administratorFullName.value || 'Not entered' }} + @if (form.controls.administratorEmail.value) { + · {{ form.controls.administratorEmail.value }} + } +
+
+
+
diff --git a/src/app/features/organizations/organization-onboarding/steps/organization-admin/organization-admin.ts b/src/app/features/organizations/organization-onboarding/steps/organization-admin/organization-admin.ts new file mode 100644 index 00000000..d9f90e20 --- /dev/null +++ b/src/app/features/organizations/organization-onboarding/steps/organization-admin/organization-admin.ts @@ -0,0 +1,96 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + inject, + input, + signal, +} from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; + +import { FormInput } from '../../../../../shared/components/form/form-input/form-input'; +import { + OnboardingStepForm, + OrganizationAdminValue, + OrganizationOnboardingData, +} from '../../models/organization-onboarding.model'; + +interface OrganizationAdminFormModel { + readonly organizationEmail: string; + readonly organizationPhone: string; + readonly administratorFullName: string; + readonly administratorEmail: string; + readonly administratorMobile: string; +} + +@Component({ + selector: 'app-organization-admin-step', + standalone: true, + imports: [ReactiveFormsModule, FormInput], + templateUrl: './organization-admin.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OrganizationAdminStepComponent implements OnboardingStepForm { + private readonly formBuilder = inject(FormBuilder); + private readonly elementRef = inject>(ElementRef); + + readonly reviewData = input.required(); + readonly submitAttempted = signal(false); + + readonly form = this.formBuilder.nonNullable.group({ + organizationEmail: ['', [Validators.required, Validators.email]], + organizationPhone: ['', [Validators.required]], + administratorFullName: ['', [Validators.required, Validators.maxLength(150)]], + administratorEmail: ['', [Validators.required, Validators.email]], + administratorMobile: ['', [Validators.required]], + }); + + validate(): boolean { + this.submitAttempted.set(true); + if (this.form.valid) { + return true; + } + + this.form.markAllAsTouched(); + this.focusFirstInvalidControl(); + return false; + } + + getValue(): OrganizationAdminValue { + const value = this.form.getRawValue() as OrganizationAdminFormModel; + return this.normalize(value); + } + + getDraftValue(): Partial { + return this.normalize(this.form.getRawValue() as OrganizationAdminFormModel); + } + + patchValue(value: Partial): void { + this.form.patchValue({ + organizationEmail: value.organizationEmail ?? '', + organizationPhone: value.organizationPhone ?? '', + administratorFullName: value.administratorFullName ?? '', + administratorEmail: value.administratorEmail ?? '', + administratorMobile: value.administratorMobile ?? '', + }, { emitEvent: false }); + this.submitAttempted.set(false); + } + + private normalize(value: OrganizationAdminFormModel): OrganizationAdminValue { + return { + organizationEmail: value.organizationEmail.trim(), + organizationPhone: value.organizationPhone.trim(), + administratorFullName: value.administratorFullName.trim(), + administratorEmail: value.administratorEmail.trim(), + administratorMobile: value.administratorMobile.trim(), + }; + } + + private focusFirstInvalidControl(): void { + queueMicrotask(() => { + this.elementRef.nativeElement + .querySelector('[data-form-control].ng-invalid, .ng-invalid [data-form-control]') + ?.focus(); + }); + } +} diff --git a/src/app/features/organizations/organization-onboarding/steps/organization-localization/organization-localization.html b/src/app/features/organizations/organization-onboarding/steps/organization-localization/organization-localization.html index 80504b6d..8de0ebdb 100644 --- a/src/app/features/organizations/organization-onboarding/steps/organization-localization/organization-localization.html +++ b/src/app/features/organizations/organization-onboarding/steps/organization-localization/organization-localization.html @@ -63,20 +63,93 @@
+
+ +
- +
+ +
+ +
+ +
+ +
+ +