350 lines
11 KiB
TypeScript
350 lines
11 KiB
TypeScript
import {
|
|
ChangeDetectionStrategy,
|
|
Component,
|
|
computed,
|
|
DestroyRef,
|
|
inject,
|
|
signal,
|
|
viewChild,
|
|
} from '@angular/core';
|
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
import { Router } from '@angular/router';
|
|
import { ToastrService } from 'ngx-toastr';
|
|
import { catchError, finalize, of } from 'rxjs';
|
|
import {
|
|
OnboardingStep,
|
|
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,
|
|
OrganizationLocalizationStepComponent,
|
|
OrganizationPlanLimitsStepComponent,
|
|
OrganizationAdminStepComponent,
|
|
ConfirmDialog,
|
|
],
|
|
templateUrl: './organization-onboarding.html',
|
|
styleUrl: './organization-onboarding.scss',
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
providers: [OrganizationOnboardingStateService, OrganizationOnboardingService]
|
|
})
|
|
export class OrganizationOnboarding {
|
|
private readonly destroyRef = inject(DestroyRef);
|
|
private readonly router = inject(Router);
|
|
private readonly toastr = inject(ToastrService);
|
|
readonly stateService = inject(OrganizationOnboardingStateService);
|
|
private readonly onboardingService = inject(OrganizationOnboardingService);
|
|
|
|
readonly steps: readonly OnboardingStep[] = this.stateService.createStepDefinitions();
|
|
|
|
readonly savingDraft = signal(false);
|
|
readonly finishing = signal(false);
|
|
|
|
private readonly basicsStep = viewChild(OrganizationBasicsStepComponent);
|
|
private readonly localizationStep = viewChild(OrganizationLocalizationStepComponent);
|
|
private readonly planLimitsStep = viewChild(OrganizationPlanLimitsStepComponent);
|
|
private readonly adminStep = viewChild(OrganizationAdminStepComponent);
|
|
private readonly cancelConfirmDialog = viewChild<ConfirmDialog>('cancelConfirmDialog');
|
|
private readonly finishConfirmDialog = viewChild<ConfirmDialog>('finishConfirmDialog');
|
|
private savedDraftSnapshot = '';
|
|
|
|
readonly provisioningRequest = signal<OrganizationProvisioningRequest | null>(null);
|
|
|
|
readonly currentStepIndex = this.stateService.currentStepIndex;
|
|
readonly completedStepIndexes = this.stateService.completedStepIndexes;
|
|
|
|
readonly currentStep = computed(
|
|
() => this.steps[this.currentStepIndex()]
|
|
);
|
|
|
|
readonly navigationDisabled = computed(
|
|
() => this.savingDraft() || this.finishing()
|
|
);
|
|
|
|
constructor() {
|
|
this.onboardingService.loadDraft()
|
|
.pipe(
|
|
catchError(() => {
|
|
this.toastr.error('Unable to restore the onboarding draft.', 'Draft restore failed');
|
|
return of(null);
|
|
}),
|
|
takeUntilDestroyed(this.destroyRef)
|
|
)
|
|
.subscribe(draft => {
|
|
if (!draft) {
|
|
this.savedDraftSnapshot = this.serializeDraftState();
|
|
return;
|
|
}
|
|
|
|
this.stateService.restoreDraft(draft);
|
|
this.savedDraftSnapshot = this.serializeDraftState();
|
|
this.hydrateActiveStep();
|
|
});
|
|
}
|
|
|
|
onStepSelected(index: number): void {
|
|
if (!this.stateService.canOpenStep(index)) {
|
|
return;
|
|
}
|
|
|
|
this.captureActiveStepDraft();
|
|
this.stateService.setCurrentStepIndex(index);
|
|
this.hydrateActiveStep();
|
|
}
|
|
|
|
onBack(): void {
|
|
const currentIndex = this.currentStepIndex();
|
|
|
|
if (currentIndex <= 0) {
|
|
return;
|
|
}
|
|
|
|
this.captureActiveStepDraft();
|
|
this.stateService.setCurrentStepIndex(currentIndex - 1);
|
|
this.hydrateActiveStep();
|
|
}
|
|
|
|
onNext(): void {
|
|
const currentIndex = this.currentStepIndex();
|
|
|
|
if (currentIndex >= this.steps.length - 1) {
|
|
return;
|
|
}
|
|
|
|
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 {
|
|
if (this.savingDraft()) {
|
|
return;
|
|
}
|
|
|
|
this.captureActiveStepDraft();
|
|
|
|
this.savingDraft.set(true);
|
|
|
|
this.onboardingService.saveDraft(this.stateService.buildDraft())
|
|
.pipe(
|
|
finalize(() => {
|
|
this.savingDraft.set(false);
|
|
}),
|
|
takeUntilDestroyed(this.destroyRef)
|
|
)
|
|
.subscribe({
|
|
next: () => {
|
|
this.savedDraftSnapshot = this.serializeDraftState();
|
|
this.toastr.success('Onboarding draft saved successfully.', 'Draft saved');
|
|
},
|
|
error: () => {
|
|
this.toastr.error('Unable to save the onboarding draft.', 'Draft save failed');
|
|
}
|
|
});
|
|
}
|
|
|
|
onFinish(): void {
|
|
if (this.finishing()) {
|
|
return;
|
|
}
|
|
|
|
const activeStep = this.adminStep();
|
|
if (!activeStep?.validate()) {
|
|
this.stateService.unmarkStepCompleted(3);
|
|
return;
|
|
}
|
|
|
|
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.captureActiveStepDraft();
|
|
if (this.serializeDraftState() !== this.savedDraftSnapshot) {
|
|
void this.cancelConfirmDialog()?.open();
|
|
return;
|
|
}
|
|
|
|
void this.leaveOnboarding(false);
|
|
}
|
|
|
|
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<unknown> | 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<unknown>): 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<void> {
|
|
if (clearDraft) {
|
|
try {
|
|
await new Promise<void>((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']);
|
|
}
|
|
}
|