organization dashboard changes

This commit is contained in:
Gagan7900
2026-07-30 20:27:50 +05:30
parent 2a6f85890f
commit 430389c611
8 changed files with 441 additions and 66 deletions
@@ -60,11 +60,11 @@
<label for="signin-password" class="block text-sm font-semibold text-slate-800">Password</label>
<a routerLink="/authentication/reset-password/basic"
class="text-xs sm:text-sm font-semibold text-[#7c3aed] hover:text-[#6d28d9] transition-colors">
Forgot password?
Forgot Password?
</a>
</div>
<div class="relative flex items-center">
<input [type]="visibilityMap['Angular'] ? 'text' : 'password'" id="signin-password" placeholder="password"
<input [type]="visibilityMap['Angular'] ? 'text' : 'password'" id="signin-password" placeholder="Password"
formControlName="password" autocomplete="current-password"
class="login-input w-full pl-4 pr-11 py-3 bg-white border border-slate-300 rounded-xl text-base font-semibold text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-purple-400/50 focus:border-purple-500 focus:bg-white transition-all duration-200" />
<button type="button" (click)="toggleVisibility('Angular')"
@@ -94,7 +94,7 @@
<input type="checkbox" id="rememberMe" formControlName="rememberMe"
class="w-4 h-4 rounded border-slate-300 text-purple-600 focus:ring-purple-500 cursor-pointer" />
<label for="rememberMe" class="text-xs sm:text-sm text-slate-700 font-semibold cursor-pointer select-none">
Remember password?
Remember Password?
</label>
</div>
@@ -1,4 +1,4 @@
<div class="md:flex block items-center justify-between my-[1.5rem] page-header-breadcrumb">
<!-- <div class="md:flex block items-center justify-between my-[1.5rem] page-header-breadcrumb">
<div>
<p class="font-semibold text-[1.125rem] text-defaulttextcolor dark:text-defaulttextcolor/70 !mb-0 ">Welcome
back,
@@ -17,13 +17,13 @@
<i class="ri-upload-cloud-line inline-block"></i>Export
</button>
</div>
</div>
</div> -->
<div class="grid grid-cols-12 gap-6">
<div class="grid grid-cols-12 gap-x-6 gap-y-4">
@for (card of statCards(); track card.title) {
<div class="xxl:col-span-2 xl:col-span-2 md:col-span-6 col-span-12">
<div [class]="'box overflow-hidden border-t-[3px] ' + card.accentClass">
<div [class]="'box overflow-hidden border-t-[3px] !mb-0 ' + card.accentClass">
<div class="box-body">
<div class="flex items-start justify-between gap-4">
<div class="text-center">
@@ -1,6 +1,7 @@
import { Component, OnInit, inject, signal } from '@angular/core';
import { Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { map, tap } from 'rxjs/operators';
import { DataTable } from '../../../shared/components/data-table/data-table';
import { DataTableStore } from '../../../shared/components/data-table/data-table.store';
@@ -8,10 +9,33 @@ import {
DataTableAction,
DataTableActionEvent,
DataTableColumn,
DataTableRecord,
} from '../../../shared/components/data-table/data-table.types';
import { OrganizationDto, OrganizationStatus, OrganizationTableRow } from '../organization-list/models/organization.model';
import {
OrganizationStatusCountsDto,
RecentOrganizationDto,
} from '../organization-list/models/organization.model';
import { OrganizationService } from '../organization-list/data-access/organization.service';
export interface RecentOrganizationTableRow extends DataTableRecord {
readonly id: string;
readonly code: string;
readonly name: string;
readonly organizationName: string;
readonly country: string;
readonly countryName: string;
readonly plan: string;
readonly planName: string;
readonly status: string;
readonly rawStatus: number | string;
readonly subscriptionStatus: string;
readonly expiry: string;
readonly planExpiresOn: string;
readonly isActive: boolean;
readonly createdOn: string;
readonly serialNumber: number;
}
interface DashboardStatCard {
title: string;
value: string;
@@ -22,6 +46,49 @@ interface DashboardStatCard {
helperClass: string;
}
function formatTenantStatus(status: number | string | undefined | null): string {
if (status === null || status === undefined) return 'Draft';
if (typeof status === 'number') {
switch (status) {
case 0: return 'Draft';
case 1: return 'Active';
case 2: return 'Suspended';
case 3: return 'Cancelled';
case 4: return 'Trial';
case 5: return 'Awaiting Database';
case 6: return 'Awaiting Activation';
default: return 'Draft';
}
}
return String(status).replace(/([a-z])([A-Z])/g, '$1 $2');
}
function formatSubscriptionStatus(subStatus: number | string | undefined | null): string {
if (subStatus === null || subStatus === undefined) return '—';
if (typeof subStatus === 'number') {
switch (subStatus) {
case 0: return 'Trialing';
case 1: return 'Active';
case 2: return 'Past Due';
case 3: return 'Cancelled';
case 4: return 'Expired';
default: return 'Active';
}
}
return String(subStatus).replace(/([a-z])([A-Z])/g, '$1 $2');
}
function formatDateDisplay(dateStr: string | undefined | null): string {
if (!dateStr) return '—';
try {
const d = new Date(dateStr);
if (isNaN(d.getTime())) return dateStr;
return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: '2-digit' });
} catch {
return dateStr;
}
}
@Component({
selector: 'dashboard',
standalone: true,
@@ -34,12 +101,15 @@ export class Dashboard implements OnInit {
private readonly router = inject(Router);
private readonly toastr = inject(ToastrService);
private readonly organizationApi = inject(OrganizationService);
readonly tableStore = inject(DataTableStore<OrganizationDto, OrganizationTableRow>);
readonly tableStore = inject(DataTableStore<RecentOrganizationDto, RecentOrganizationTableRow>);
readonly selectedMonths = signal<number | undefined>(undefined);
readonly selectedLimit = signal<number | undefined>(undefined);
readonly statCards = signal<DashboardStatCard[]>([
{
title: 'Total Organizations',
value: '24',
value: '0',
helper: 'All registered',
accentClass: 'border-primary',
iconClass: 'ti ti-building-community',
@@ -48,7 +118,7 @@ export class Dashboard implements OnInit {
},
{
title: 'Active',
value: '19',
value: '0',
helper: 'Currently operational',
accentClass: 'border-primary',
iconClass: 'ti ti-circle-check',
@@ -57,16 +127,25 @@ export class Dashboard implements OnInit {
},
{
title: 'Trial',
value: '3',
value: '0',
helper: 'Trial subscriptions',
accentClass: 'border-primary',
iconClass: 'ri-wallet-2-line',
iconBackgroundClass: 'bg-primary',
helperClass: 'bg-primary/10 text-primary',
},
{
title: 'Draft',
value: '0',
helper: 'Incomplete onboarding',
accentClass: 'border-primary',
iconClass: 'ti ti-file-text',
iconBackgroundClass: 'bg-primary',
helperClass: 'bg-primary/10 text-primary',
},
{
title: 'Suspended',
value: '1',
value: '0',
helper: 'Temporarily disabled',
accentClass: 'border-primary',
iconClass: 'ti ti-player-pause',
@@ -74,26 +153,35 @@ export class Dashboard implements OnInit {
helperClass: 'bg-primary/10 text-primary',
},
{
title: 'Expired License',
value: '1',
helper: 'Needs renewal action',
title: 'Cancelled',
value: '0',
helper: 'Cancelled subscriptions',
accentClass: 'border-primary',
iconClass: 'ri ri-pass-expired-line',
iconBackgroundClass: 'bg-primary',
helperClass: 'bg-primary/10 text-primary',
},
{
title: 'Active Users',
value: '482',
helper: 'Users with access',
accentClass: 'border-primary',
iconClass: 'ri ri-group-line',
iconClass: 'ti ti-square-x',
iconBackgroundClass: 'bg-primary',
helperClass: 'bg-primary/10 text-primary',
},
// {
// title: 'Awaiting Database',
// value: '0',
// helper: 'DB provisioning pending',
// accentClass: 'border-primary',
// iconClass: 'ti ti-database',
// iconBackgroundClass: 'bg-primary',
// helperClass: 'bg-primary/10 text-primary',
// },
// {
// title: 'Awaiting Activation',
// value: '0',
// helper: 'Activation pending',
// accentClass: 'border-primary',
// iconClass: 'ti ti-clock-check',
// iconBackgroundClass: 'bg-primary',
// helperClass: 'bg-primary/10 text-primary',
// },
]);
readonly columns = signal<DataTableColumn<OrganizationTableRow>[]>([
readonly columns = signal<DataTableColumn<RecentOrganizationTableRow>[]>([
{ key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '90px' },
{ key: 'code', label: 'Code', header: 'Code', sortable: true },
{
@@ -111,7 +199,7 @@ export class Dashboard implements OnInit {
sortable: true,
badge: true,
badgeClass: value =>
value === 'Trial' || value === OrganizationStatus.Trial
value === 'Trial' || value === 'Basic'
? 'badge bg-warning/10 text-warning'
: 'badge bg-light text-defaulttextcolor',
},
@@ -122,9 +210,24 @@ export class Dashboard implements OnInit {
sortable: true,
badge: true,
badgeClass: value =>
value === 'Active' || value === OrganizationStatus.Active
value === 'Active'
? 'badge bg-success/10 text-success'
: value === 'Trial' || value === OrganizationStatus.Trial
: value === 'Trial'
? 'badge bg-warning/10 text-warning'
: value === 'Draft'
? 'badge bg-info/10 text-info'
: 'badge bg-danger/10 text-danger',
},
{
key: 'subscriptionStatus',
label: 'Subscription',
header: 'Subscription',
sortable: true,
badge: true,
badgeClass: value =>
value === 'Active'
? 'badge bg-success/10 text-success'
: value === 'Trialing'
? 'badge bg-warning/10 text-warning'
: 'badge bg-danger/10 text-danger',
},
@@ -134,7 +237,7 @@ export class Dashboard implements OnInit {
readonly emptyMessage = signal<string>('No Organizations');
readonly emptyDescription = signal<string>('Start by adding your first organization');
readonly actions = signal<DataTableAction<OrganizationTableRow>[]>([
readonly actions = signal<DataTableAction<RecentOrganizationTableRow>[]>([
{ type: 'view', label: 'View', icon: 'ti ti-eye', className: 'text-info' },
{ type: 'edit', label: 'Edit', icon: 'ti ti-edit', className: 'text-primary' },
{
@@ -142,22 +245,66 @@ export class Dashboard implements OnInit {
label: 'Suspend',
icon: 'ti ti-player-pause',
className: 'text-warning',
visible: row => row.status !== 'Suspended' && row.status !== OrganizationStatus.Suspended,
visible: row => row.status !== 'Suspended',
},
]);
ngOnInit(): void {
this.tableStore.initialize({
fetcher: query => this.organizationApi.getOrganizationDataTable(query),
mapRow: (item, serialNumber) => {
const statusText = typeof item.status === 'number'
? (OrganizationStatus[item.status] ?? 'Active')
: (item.status || 'Active');
fetcher: query =>
this.organizationApi.getRecentOrganizationsDashboard(this.selectedMonths(), this.selectedLimit()).pipe(
tap(res => {
if (res?.counts) {
this.updateStatCards(res.counts);
}
}),
map(res => {
let orgs = [...(res.organizations || [])];
const total = orgs.length;
const resolvedName = item.name || item.organizationName || '—';
const resolvedCountry = item.countryName || item.country || '—';
const resolvedPlan = item.planName || item.plan || '—';
const resolvedExpiry = item.expiryDate || item.expiry || '—';
if (query.search) {
const term = query.search.toLowerCase().trim();
orgs = orgs.filter(o =>
(o.name && o.name.toLowerCase().includes(term)) ||
(o.code && o.code.toLowerCase().includes(term)) ||
(o.countryName && o.countryName.toLowerCase().includes(term)) ||
(o.planName && o.planName.toLowerCase().includes(term))
);
}
const filtered = orgs.length;
if (query.sortBy) {
const sortKey = query.sortBy as keyof RecentOrganizationDto;
const dir = query.sortDir === 'desc' ? -1 : 1;
orgs.sort((a: any, b: any) => {
const valA = a[sortKey] ?? '';
const valB = b[sortKey] ?? '';
if (typeof valA === 'string' && typeof valB === 'string') {
return valA.localeCompare(valB) * dir;
}
return (valA > valB ? 1 : valA < valB ? -1 : 0) * dir;
});
}
const start = (query.page - 1) * query.pageSize;
const pagedOrgs = orgs.slice(start, start + query.pageSize);
return {
draw: query.draw,
rows: pagedOrgs as RecentOrganizationDto[],
total,
filtered,
};
})
),
mapRow: (item, serialNumber) => {
const statusText = formatTenantStatus(item.status);
const subStatusText = formatSubscriptionStatus(item.subscriptionStatus);
const resolvedName = item.name || '—';
const resolvedCountry = item.countryName || '—';
const resolvedPlan = item.planName || '—';
const resolvedExpiry = item.planExpiresOn ? String(item.planExpiresOn) : '—';
return {
...item,
@@ -165,31 +312,109 @@ export class Dashboard implements OnInit {
code: item.code || '—',
name: resolvedName,
organizationName: resolvedName,
countryId: item.countryId || null,
country: resolvedCountry,
countryName: resolvedCountry,
plan: resolvedPlan,
planName: resolvedPlan,
status: statusText,
rawStatus: item.status,
subscriptionStatus: subStatusText,
expiry: resolvedExpiry,
expiryDate: resolvedExpiry,
dataRegion: item.dataRegion || null,
planExpiresOn: resolvedExpiry,
isActive: item.isActive ?? true,
createdOn: item.createdOn ? formatDateDisplay(item.createdOn) : '—',
serialNumber,
};
},
onError: (err: any) => {
const msg = err?.error?.message || err?.error?.title || 'Failed to load dashboard organizations.';
this.toastr.error(msg);
}
},
});
}
private updateStatCards(counts: OrganizationStatusCountsDto): void {
this.statCards.set([
{
title: 'Total Organizations',
value: String(counts.totalOrganizations ?? 0),
helper: 'All registered',
accentClass: 'border-primary',
iconClass: 'ti ti-building-community',
iconBackgroundClass: 'bg-primary',
helperClass: 'bg-primary/10 text-primary',
},
{
title: 'Active',
value: String(counts.active ?? 0),
helper: 'Currently operational',
accentClass: 'border-primary',
iconClass: 'ti ti-circle-check',
iconBackgroundClass: 'bg-primary',
helperClass: 'bg-primary/10 text-primary',
},
{
title: 'Trial',
value: String(counts.trial ?? 0),
helper: 'Trial subscriptions',
accentClass: 'border-primary',
iconClass: 'ri-wallet-2-line',
iconBackgroundClass: 'bg-primary',
helperClass: 'bg-primary/10 text-primary',
},
{
title: 'Draft',
value: String(counts.draft ?? 0),
helper: 'Incomplete onboarding',
accentClass: 'border-primary',
iconClass: 'ti ti-file-text',
iconBackgroundClass: 'bg-primary',
helperClass: 'bg-primary/10 text-primary',
},
{
title: 'Suspended',
value: String(counts.suspended ?? 0),
helper: 'Temporarily disabled',
accentClass: 'border-primary',
iconClass: 'ti ti-player-pause',
iconBackgroundClass: 'bg-primary',
helperClass: 'bg-primary/10 text-primary',
},
{
title: 'Cancelled',
value: String(counts.cancelled ?? 0),
helper: 'Cancelled subscriptions',
accentClass: 'border-primary',
iconClass: 'ti ti-square-x',
iconBackgroundClass: 'bg-primary',
helperClass: 'bg-primary/10 text-primary',
},
// {
// title: 'Awaiting Database',
// value: String(counts.awaitingDatabase ?? 0),
// helper: 'DB provisioning pending',
// accentClass: 'border-primary',
// iconClass: 'ti ti-database',
// iconBackgroundClass: 'bg-primary',
// helperClass: 'bg-primary/10 text-primary',
// },
// {
// title: 'Awaiting Activation',
// value: String(counts.awaitingActivation ?? 0),
// helper: 'Activation pending',
// accentClass: 'border-primary',
// iconClass: 'ti ti-clock-check',
// iconBackgroundClass: 'bg-primary',
// helperClass: 'bg-primary/10 text-primary',
// },
]);
}
onAddOrganization(): void {
void this.router.navigate(['/organizations/onboarding']);
}
onActionClick(event: DataTableActionEvent<OrganizationTableRow>): void {
onActionClick(event: DataTableActionEvent<RecentOrganizationTableRow>): void {
if (event.action.type === 'edit' || (event.row.status as string) === 'Draft') {
void this.router.navigate(['/organizations/onboarding'], { queryParams: { id: event.row.id } });
return;
@@ -197,5 +422,3 @@ export class Dashboard implements OnInit {
this.toastr.info(`${event.action.label} clicked for ${event.row.organizationName}`);
}
}
@@ -16,5 +16,10 @@ export const ORGANIZATION_ENDPOINTS = {
'masterAdmin',
'/v1/tenants/autocomplete'
),
dashboard: buildApiUrl(
'masterAdmin',
'/v1/organizations/dashboard'
),
} as const;
@@ -7,7 +7,8 @@ import { DataTableQuery, DataTableResult } from '../../../../shared/components/d
import {
OrganizationDto,
OrganizationLookupDto,
OrganizationTableRow
OrganizationTableRow,
RecentOrganizationsDashboardDto
} from '../models/organization.model';
@Injectable({
@@ -16,6 +17,17 @@ import {
export class OrganizationService {
private readonly http = inject(HttpClient);
getRecentOrganizationsDashboard(months?: number, limit?: number): Observable<RecentOrganizationsDashboardDto> {
let params = new HttpParams();
if (months !== undefined && months !== null) {
params = params.set('months', months.toString());
}
if (limit !== undefined && limit !== null) {
params = params.set('limit', limit.toString());
}
return this.http.get<RecentOrganizationsDashboardDto>(ORGANIZATION_ENDPOINTS.dashboard, { params });
}
getOrganizationDataTable(query: DataTableQuery, countryId?: string | null): Observable<DataTableResult<OrganizationDto>> {
const payload = {
...query,
@@ -65,3 +65,40 @@ export interface OrganizationTableRow extends DataTableRecord {
export type OrganizationModalMode = 'create' | 'edit' | 'view';
export interface OrganizationStatusCountsDto {
totalOrganizations: number;
trial: number;
active: number;
suspended: number;
cancelled: number;
draft: number;
awaitingDatabase: number;
awaitingActivation: number;
}
export interface RecentOrganizationDto {
id: string;
code: string;
name: string;
status: number | string;
countryId?: string | null;
countryName?: string | null;
planId?: string | null;
planCode?: string | null;
planName?: string | null;
subscriptionStatus?: number | string | null;
planExpiresOn?: string | null;
isActive: boolean;
createdOn: string;
}
export interface RecentOrganizationsDashboardDto {
months: number;
limit: number;
from: string;
generatedOn: string;
counts: OrganizationStatusCountsDto;
organizations: RecentOrganizationDto[];
}
@@ -33,11 +33,6 @@ const INITIAL_DRAFT_STATE: OnboardingDraftState = {
admin: null,
};
/**
* The onboarding API mixes PascalCase (write requests) and camelCase (typed response fields)
* for the same logical data. This raw shape lets restoreServerDraft() defensively read both
* casings from a server draft payload without resorting to `any`.
*/
type RawDraftRecord = Record<string, unknown>;
function firstDefined(...values: readonly unknown[]): unknown {
@@ -233,9 +228,21 @@ export class OrganizationOnboardingStateService {
admin: adminData ? { ...adminData } : null,
});
const completedIndexes = isEditMode
? [0, 1, 2, 3]
: (serverDraft.completedStepIndexes ?? []);
const statusStr = String(serverDraft.status || rawRecord['status'] || rawRecord['Status'] || '').toLowerCase();
const isNonDraftActive = isEditMode && statusStr !== '' && statusStr !== 'draft' && statusStr !== '0';
const explicitIndexes = serverDraft.completedStepIndexes ?? [];
const isStep0Done = isNonDraftActive || evaluateSectionCompletion(basicsData, 'basics', rawRecord, explicitIndexes, 0);
const isStep1Done = isNonDraftActive || evaluateSectionCompletion(localizationData, 'localization', rawRecord, explicitIndexes, 1);
const isStep2Done = isNonDraftActive || evaluateSectionCompletion(planLimitsData, 'plan', rawRecord, explicitIndexes, 2) || evaluateSectionCompletion(planLimitsData, 'planLimits', rawRecord, explicitIndexes, 2);
const isStep3Done = isNonDraftActive || evaluateSectionCompletion(adminData, 'admin', rawRecord, explicitIndexes, 3) || evaluateSectionCompletion(adminData, 'adminContact', rawRecord, explicitIndexes, 3);
const completedIndexes: number[] = [];
if (isStep0Done) completedIndexes.push(0);
if (isStep1Done) completedIndexes.push(1);
if (isStep2Done) completedIndexes.push(2);
if (isStep3Done) completedIndexes.push(3);
this.onboardingDataState.set({
// Trust boundary: the server draft payload is only validated at runtime by the API,
@@ -254,8 +261,15 @@ export class OrganizationOnboardingStateService {
: null,
});
const stepIndex = typeof serverDraft.currentStepIndex === 'number' ? serverDraft.currentStepIndex : 0;
this.currentStepIndexState.set(Math.min(3, Math.max(0, stepIndex)));
let initialStepIndex = 0;
if (typeof serverDraft.currentStepIndex === 'number' && serverDraft.currentStepIndex > 0 && serverDraft.currentStepIndex <= 3) {
initialStepIndex = serverDraft.currentStepIndex;
} else {
const firstUncompletedIndex = [0, 1, 2, 3].find(idx => !completedIndexes.includes(idx));
initialStepIndex = firstUncompletedIndex !== undefined ? firstUncompletedIndex : 0;
}
this.currentStepIndexState.set(Math.min(3, Math.max(0, initialStepIndex)));
this.setCompletedStepIndexes(completedIndexes);
}
@@ -311,3 +325,57 @@ export class OrganizationOnboardingStateService {
];
}
}
function isValidDateOrTrue(val: unknown): boolean {
if (val === true || val === 1 || val === '1' || val === 'true' || val === 'True') {
return true;
}
if (typeof val === 'string' && val.trim().length > 0) {
const d = Date.parse(val);
return !isNaN(d);
}
return false;
}
function evaluateSectionCompletion(
sectionData: RawDraftRecord | null,
sectionKey: string,
rawRecord: RawDraftRecord,
explicitCompletedIndexes?: readonly number[],
stepIndex?: number
): boolean {
if (explicitCompletedIndexes && typeof stepIndex === 'number' && explicitCompletedIndexes.includes(stepIndex)) {
return true;
}
if (sectionData) {
const sectionCompletionVal = firstDefined(
sectionData['completedAt'], sectionData['CompletedAt'],
sectionData['completedOn'], sectionData['CompletedOn'],
sectionData['markComplete'], sectionData['MarkComplete'],
sectionData['isCompleted'], sectionData['IsCompleted']
);
if (isValidDateOrTrue(sectionCompletionVal)) {
return true;
}
}
const topKeyCap = capitalize(sectionKey);
const topLevelVal = firstDefined(
rawRecord[`${sectionKey}CompletedAt`], rawRecord[`${topKeyCap}CompletedAt`],
rawRecord[`${sectionKey}CompletedOn`], rawRecord[`${topKeyCap}CompletedOn`],
rawRecord[`is${topKeyCap}Completed`], rawRecord[`Is${topKeyCap}Completed`],
rawRecord[`${sectionKey}MarkedComplete`], rawRecord[`${topKeyCap}MarkedComplete`]
);
if (isValidDateOrTrue(topLevelVal)) {
return true;
}
return false;
}
function capitalize(str: string): string {
return str ? str.charAt(0).toUpperCase() + str.slice(1) : '';
}
@@ -279,19 +279,25 @@ export class OrganizationOnboarding {
case 1: {
const data = (activeStepComponent as OrganizationLocalizationStepComponent).getValue();
const payload = mapLocalizationStepToApiRequest(data, true);
update$ = this.onboardingService.updateLocalization(orgId, payload);
update$ = this.ensureBasicsCompleted(orgId).pipe(
switchMap(() => this.onboardingService.updateLocalization(orgId, payload))
);
break;
}
case 2: {
const data = (activeStepComponent as OrganizationPlanLimitsStepComponent).getValue();
const payload = mapPlanStepToApiRequest(data, true);
update$ = this.onboardingService.updatePlan(orgId, payload);
update$ = this.ensureBasicsCompleted(orgId).pipe(
switchMap(() => this.onboardingService.updatePlan(orgId, payload))
);
break;
}
case 3: {
const data = (activeStepComponent as OrganizationAdminStepComponent).getValue();
const payload = mapAdminContactStepToApiRequest(data, true);
update$ = this.onboardingService.updateAdminContact(orgId, payload);
update$ = this.ensureBasicsCompleted(orgId).pipe(
switchMap(() => this.onboardingService.updateAdminContact(orgId, payload))
);
break;
}
default:
@@ -343,13 +349,31 @@ export class OrganizationOnboarding {
this.executeFinishAndProvisionFlow(orgId, adminData);
}
private ensureBasicsCompleted(orgId: string): Observable<unknown> {
const basicsDraft = this.basicsStep()?.getDraftValue()
?? this.stateService.basics()
?? this.stateService.onboardingDraft().basics;
if (basicsDraft && (basicsDraft.organizationName || (basicsDraft as any).name || (basicsDraft as any).Name)) {
const payload = mapBasicsStepToApiRequest(basicsDraft, true);
return this.onboardingService.updateBasics(orgId, payload).pipe(
catchError(err => {
console.warn('Unable to mark basics step completed on server prior to updating subsequent step:', err);
return of(null);
})
);
}
return of(null);
}
private executeFinishAndProvisionFlow(orgId: string, adminData: OrganizationAdminValue): void {
this.finishing.set(true);
const adminPayload = mapAdminContactStepToApiRequest(adminData, true);
this.onboardingService.updateAdminContact(orgId, adminPayload)
this.ensureBasicsCompleted(orgId)
.pipe(
switchMap(() => this.onboardingService.updateAdminContact(orgId, adminPayload)),
switchMap(() => this.onboardingService.finishOnboarding(orgId)),
finalize(() => {
this.finishing.set(false);
@@ -407,17 +431,23 @@ export class OrganizationOnboarding {
case 1: {
const loc = this.localizationStep()?.getDraftValue() ?? {};
const requestPayload = mapLocalizationStepToApiRequest(loc, markComplete);
return this.onboardingService.updateLocalization(orgId, requestPayload);
return this.ensureBasicsCompleted(orgId).pipe(
switchMap(() => this.onboardingService.updateLocalization(orgId, requestPayload))
) as Observable<OrganizationServerDraftResponse>;
}
case 2: {
const plan = this.planLimitsStep()?.getDraftValue() ?? {};
const requestPayload = mapPlanStepToApiRequest(plan, markComplete);
return this.onboardingService.updatePlan(orgId, requestPayload);
return this.ensureBasicsCompleted(orgId).pipe(
switchMap(() => this.onboardingService.updatePlan(orgId, requestPayload))
) as Observable<OrganizationServerDraftResponse>;
}
case 3: {
const admin = this.adminStep()?.getDraftValue() ?? {};
const requestPayload = mapAdminContactStepToApiRequest(admin, markComplete);
return this.onboardingService.updateAdminContact(orgId, requestPayload);
return this.ensureBasicsCompleted(orgId).pipe(
switchMap(() => this.onboardingService.updateAdminContact(orgId, requestPayload))
) as Observable<OrganizationServerDraftResponse>;
}
default:
return of({ id: orgId, status: 'Draft' });