organization onboarding list, edit case changes

This commit is contained in:
Gagan7900
2026-07-30 14:30:51 +05:30
parent 4838e16dc8
commit 368d6bd540
23 changed files with 746 additions and 445 deletions
@@ -54,7 +54,7 @@
[columns]="columns()"
[rows]="tableStore.rows()"
[actions]="actions()"
[totalRecords]="tableStore.totalRecords()"
[totalRecords]="tableStore.filteredRecords()"
[pageIndex]="tableStore.queryState.pageIndex()"
[pageSize]="tableStore.queryState.pageSize()"
tableTitle="Recently Created Organizations"
@@ -1,7 +1,6 @@
import { Component, OnInit, inject, signal } from '@angular/core';
import { Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { of } from 'rxjs';
import { DataTable } from '../../../shared/components/data-table/data-table';
import { DataTableStore } from '../../../shared/components/data-table/data-table.store';
@@ -9,12 +8,9 @@ import {
DataTableAction,
DataTableActionEvent,
DataTableColumn,
DataTableRecord,
DataTableResult,
} from '../../../shared/components/data-table/data-table.types';
type OrganizationPlan = 'Enterprise' | 'Standard' | 'Trial';
type OrganizationStatus = 'Active' | 'Trial' | 'Suspended';
import { OrganizationDto, OrganizationStatus, OrganizationTableRow } from '../organization-list/models/organization.model';
import { OrganizationService } from '../organization-list/data-access/organization.service';
interface DashboardStatCard {
title: string;
@@ -25,17 +21,6 @@ interface DashboardStatCard {
iconBackgroundClass: string;
helperClass: string;
}
interface OrganizationListRow extends DataTableRecord {
id: string;
code: string;
organizationName: string;
countryId: string;
country: string;
plan: string;
status: OrganizationStatus;
expiry: string;
}
@Component({
selector: 'dashboard',
@@ -48,7 +33,8 @@ interface OrganizationListRow extends DataTableRecord {
export class Dashboard implements OnInit {
private readonly router = inject(Router);
private readonly toastr = inject(ToastrService);
readonly tableStore = inject(DataTableStore<OrganizationListRow, OrganizationListRow>);
private readonly organizationApi = inject(OrganizationService);
readonly tableStore = inject(DataTableStore<OrganizationDto, OrganizationTableRow>);
readonly statCards = signal<DashboardStatCard[]>([
{
@@ -107,100 +93,8 @@ export class Dashboard implements OnInit {
},
]);
readonly recentOrganizations = signal<OrganizationListRow[]>([
{
id: '1',
code: 'ORG-0024',
organizationName: 'Syscom Group',
countryId: 'c1',
country: 'Saudi Arabia',
plan: 'Enterprise',
status: 'Active',
expiry: '31-12-2026'
},
{
id: '2',
code: 'ORG-0023',
organizationName: 'Acme Trading',
countryId: 'c2',
country: 'India',
plan: 'Standard',
status: 'Active',
expiry: '31-03-2027'
},
{
id: '3',
code: 'ORG-0022',
organizationName: 'Falcon Retail LLC',
countryId: 'c3',
country: 'UAE',
plan: 'Trial',
status: 'Trial',
expiry: '28-07-2026'
},
{
id: '4',
code: 'ORG-0021',
organizationName: 'Oasis Foods',
countryId: 'c1',
country: 'Saudi Arabia',
plan: 'Standard',
status: 'Suspended',
expiry: '15-06-2026'
},
{
id: '5',
code: 'ORG-0022',
organizationName: 'Oasis Foods',
countryId: 'c1',
country: 'Saudi Arabia',
plan: 'Standard',
status: 'Suspended',
expiry: '15-06-2026'
},
{
id: '6',
code: 'ORG-0023',
organizationName: 'Oasis Foods',
countryId: 'c1',
country: 'Saudi Arabia',
plan: 'Standard',
status: 'Suspended',
expiry: '15-06-2026'
},
{
id: '7',
code: 'ORG-0024',
organizationName: 'Oasis Foods',
countryId: 'c1',
country: 'Saudi Arabia',
plan: 'Standard',
status: 'Suspended',
expiry: '15-06-2026'
},
{
id: '8',
code: 'ORG-0025',
organizationName: 'Oasis Foods',
countryId: 'c1',
country: 'Saudi Arabia',
plan: 'Standard',
status: 'Suspended',
expiry: '15-06-2026'
},
{
id: '9',
code: 'ORG-0026',
organizationName: 'Oasis Foods',
countryId: 'c1',
country: 'Saudi Arabia',
plan: 'Standard',
status: 'Suspended',
expiry: '15-06-2026'
},
]);
readonly columns = signal<DataTableColumn<OrganizationListRow>[]>([
readonly columns = signal<DataTableColumn<OrganizationTableRow>[]>([
{ key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '90px' },
{ key: 'code', label: 'Code', header: 'Code', sortable: true },
{
key: 'organizationName',
@@ -217,7 +111,7 @@ export class Dashboard implements OnInit {
sortable: true,
badge: true,
badgeClass: value =>
value === 'Trial'
value === 'Trial' || value === OrganizationStatus.Trial
? 'badge bg-warning/10 text-warning'
: 'badge bg-light text-defaulttextcolor',
},
@@ -228,9 +122,9 @@ export class Dashboard implements OnInit {
sortable: true,
badge: true,
badgeClass: value =>
value === 'Active'
value === 'Active' || value === OrganizationStatus.Active
? 'badge bg-success/10 text-success'
: value === 'Trial'
: value === 'Trial' || value === OrganizationStatus.Trial
? 'badge bg-warning/10 text-warning'
: 'badge bg-danger/10 text-danger',
},
@@ -240,7 +134,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<OrganizationListRow>[]>([
readonly actions = signal<DataTableAction<OrganizationTableRow>[]>([
{ type: 'view', label: 'View', icon: 'ti ti-eye', className: 'text-info' },
{ type: 'edit', label: 'Edit', icon: 'ti ti-edit', className: 'text-primary' },
{
@@ -248,47 +142,45 @@ export class Dashboard implements OnInit {
label: 'Suspend',
icon: 'ti ti-player-pause',
className: 'text-warning',
visible: row => row.status !== 'Suspended',
visible: row => row.status !== 'Suspended' && row.status !== OrganizationStatus.Suspended,
},
]);
ngOnInit(): void {
this.tableStore.initialize({
fetcher: query => {
const search = (query.search || '').trim().toLowerCase();
const sortBy = query.sortBy;
const sortDir = query.sortDir;
fetcher: query => this.organizationApi.getOrganizationDataTable(query),
mapRow: (item, serialNumber) => {
const statusText = typeof item.status === 'number'
? (OrganizationStatus[item.status] ?? 'Active')
: (item.status || 'Active');
let rows = this.recentOrganizations().filter(row => {
const matchesSearch = !search
|| row.code.toLowerCase().includes(search)
|| row.organizationName.toLowerCase().includes(search)
|| row.country.toLowerCase().includes(search)
|| row.plan.toLowerCase().includes(search)
|| row.status.toLowerCase().includes(search)
|| (row.expiry && row.expiry.toLowerCase().includes(search));
return matchesSearch;
});
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 (sortBy) {
rows = [...rows].sort((left, right) => {
const leftVal = String(left[sortBy as keyof OrganizationListRow] ?? '').toLowerCase();
const rightVal = String(right[sortBy as keyof OrganizationListRow] ?? '').toLowerCase();
const compared = leftVal.localeCompare(rightVal);
return sortDir === 'asc' ? compared : -compared;
});
}
const total = rows.length;
const start = (query.page - 1) * query.pageSize;
const end = start + query.pageSize;
const result: DataTableResult<OrganizationListRow> = {
draw: query.draw,
total,
filtered: total,
rows: rows.slice(start, end)
return {
...item,
id: item.id,
code: item.code || '',
name: resolvedName,
organizationName: resolvedName,
countryId: item.countryId || null,
country: resolvedCountry,
countryName: resolvedCountry,
plan: resolvedPlan,
planName: resolvedPlan,
status: statusText,
expiry: resolvedExpiry,
expiryDate: resolvedExpiry,
dataRegion: item.dataRegion || null,
isActive: item.isActive ?? true,
serialNumber,
};
return of(result);
},
onError: (err: any) => {
const msg = err?.error?.message || err?.error?.title || 'Failed to load dashboard organizations.';
this.toastr.error(msg);
}
});
}
@@ -297,7 +189,7 @@ export class Dashboard implements OnInit {
void this.router.navigate(['/organizations/onboarding']);
}
onActionClick(event: DataTableActionEvent<OrganizationListRow>): void {
onActionClick(event: DataTableActionEvent<OrganizationTableRow>): void {
if (event.action.type === 'edit' || (event.row.status as string) === 'Draft') {
void this.router.navigate(['/organizations/onboarding'], { queryParams: { id: event.row.id } });
return;
@@ -306,3 +198,4 @@ export class Dashboard implements OnInit {
}
}
@@ -0,0 +1,20 @@
import { buildApiUrl } from '../../../../core/config/api-url.util';
export const ORGANIZATION_ENDPOINTS = {
dataTable: buildApiUrl(
'masterAdmin',
'/v1/tenants/datatable'
),
getById: (id: string) =>
buildApiUrl(
'masterAdmin',
`/v1/tenants/${encodeURIComponent(id)}`
),
autocomplete: buildApiUrl(
'masterAdmin',
'/v1/tenants/autocomplete'
),
} as const;
@@ -0,0 +1,41 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { ORGANIZATION_ENDPOINTS } from './organization.endpoints';
import { DataTableQuery, DataTableResult } from '../../../../shared/components/data-table/data-table.types';
import {
OrganizationDto,
OrganizationLookupDto,
OrganizationTableRow
} from '../models/organization.model';
@Injectable({
providedIn: 'root'
})
export class OrganizationService {
private readonly http = inject(HttpClient);
getOrganizationDataTable(query: DataTableQuery, countryId?: string | null): Observable<DataTableResult<OrganizationDto>> {
const payload = {
...query,
...(countryId ? { countryId } : {})
};
return this.http.post<DataTableResult<OrganizationDto>>(ORGANIZATION_ENDPOINTS.dataTable, payload);
}
getOrganizationById(id: string): Observable<OrganizationDto> {
return this.http.get<OrganizationDto>(ORGANIZATION_ENDPOINTS.getById(id));
}
autocomplete(term?: string, limit = 10): Observable<readonly OrganizationLookupDto[]> {
let params = new HttpParams().set('limit', limit);
const normalizedTerm = term?.trim();
if (normalizedTerm) {
params = params.set('term', normalizedTerm);
}
return this.http.get<readonly OrganizationLookupDto[]>(ORGANIZATION_ENDPOINTS.autocomplete, { params });
}
}
@@ -0,0 +1,67 @@
import { DataTableRecord } from '../../../../shared/components/data-table/data-table.types';
export enum OrganizationStatus { Trial = 0, Active = 1, Suspended = 2, Cancelled = 3 }
export interface OrganizationDto {
id: string;
code: string;
name: string;
organizationName?: string | null;
status: OrganizationStatus | string;
countryId?: string | null;
countryName?: string | null;
country?: string | null;
planId?: string | null;
planName?: string | null;
plan?: string | null;
expiryDate?: string | null;
expiry?: string | null;
defaultLanguageId?: string | null;
defaultLanguageName?: string | null;
defaultDbConnectionId?: string | null;
defaultDbConnectionName?: string | null;
defaultCurrencyId?: string | null;
defaultCurrencyName?: string | null;
defaultTimezoneId?: string | null;
defaultTimezoneName?: string | null;
dataRegion?: string | null;
isActive?: boolean;
createdOn?: string;
modifiedOn?: string | null;
}
export interface OrganizationLookupDto {
id: string;
name: string;
code: string;
}
export interface OrganizationTableRow extends DataTableRecord {
readonly id: string;
readonly code: string;
readonly name: string;
readonly organizationName: string;
readonly countryId: string | null;
readonly country: string;
readonly countryName: string | null;
readonly plan: string;
readonly planName: string | null;
readonly status: string | OrganizationStatus;
readonly expiry: string;
readonly expiryDate: string | null;
readonly dataRegion: string | null;
readonly isActive: boolean;
readonly serialNumber: number;
readonly defaultLanguageId?: string | null;
readonly defaultCurrencyId?: string | null;
readonly defaultTimezoneId?: string | null;
readonly defaultDbConnectionId?: string | null;
readonly createdOn?: string;
readonly modifiedOn?: string | null;
readonly defaultLanguageName?: string | null;
readonly defaultCurrencyName?: string | null;
readonly defaultTimezoneName?: string | null;
}
export type OrganizationModalMode = 'create' | 'edit' | 'view';
@@ -2,7 +2,7 @@
[columns]="columns()"
[rows]="tableStore.rows()"
[actions]="actions()"
[totalRecords]="tableStore.totalRecords()"
[totalRecords]="tableStore.filteredRecords()"
[pageIndex]="tableStore.queryState.pageIndex()"
[pageSize]="tableStore.queryState.pageSize()"
tableTitle="Organizations"
@@ -2,7 +2,6 @@ import { Component, OnInit, inject, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { of } from 'rxjs';
import { DataTable, DataTableToolbarDirective } from '../../../shared/components/data-table/data-table';
import { DataTableStore } from '../../../shared/components/data-table/data-table.store';
@@ -10,31 +9,17 @@ import {
DataTableAction,
DataTableActionEvent,
DataTableColumn,
DataTableRecord,
DataTableResult
} from '../../../shared/components/data-table/data-table.types';
import { Autocomplete } from '../../../shared/components/form/autocomplete/autocomplete';
import {
AutocompleteDisplayFn,
AutocompleteResolveValueFn,
AutocompleteSearchFn,
AutocompleteValueFn,
} from '../../../shared/components/form/autocomplete/autocomplete.types';
import { Button } from '../../../shared/components/button/button';
import { CountryLookupDto, CountryService } from '../../global-masters/countries/public-api';
type OrganizationStatus = 'Active' | 'Trial' | 'Suspended';
interface OrganizationListRow extends DataTableRecord {
id: string;
code: string;
organizationName: string;
countryId: string;
country: string;
plan: string;
status: OrganizationStatus;
expiry: string;
}
import { OrganizationDto, OrganizationStatus, OrganizationTableRow } from './models/organization.model';
import { OrganizationService } from './data-access/organization.service';
@Component({
selector: 'organization-list',
@@ -49,7 +34,8 @@ export class OrganizationList implements OnInit {
private readonly router = inject(Router);
private readonly toastr = inject(ToastrService);
private readonly countryApi = inject(CountryService);
readonly tableStore = inject(DataTableStore<OrganizationListRow, OrganizationListRow>);
private readonly organizationApi = inject(OrganizationService);
readonly tableStore = inject(DataTableStore<OrganizationDto, OrganizationTableRow>);
readonly selectedCountryLookup = signal<CountryLookupDto | null>(null);
readonly appliedCountryId = signal<string | null>(null);
@@ -59,104 +45,11 @@ export class OrganizationList implements OnInit {
countryId: [''],
});
readonly allOrganizations = signal<OrganizationListRow[]>([
{
id: '1',
code: 'ORG-0024',
organizationName: 'Syscom Group',
countryId: 'c1',
country: 'Saudi Arabia',
plan: 'Enterprise',
status: 'Active',
expiry: '31-12-2026'
},
{
id: '2',
code: 'ORG-0023',
organizationName: 'Acme Trading',
countryId: 'c2',
country: 'India',
plan: 'Standard',
status: 'Active',
expiry: '31-03-2027'
},
{
id: '3',
code: 'ORG-0022',
organizationName: 'Falcon Retail LLC',
countryId: 'c3',
country: 'UAE',
plan: 'Trial',
status: 'Trial',
expiry: '28-07-2026'
},
{
id: '4',
code: 'ORG-0021',
organizationName: 'Oasis Foods',
countryId: 'c1',
country: 'Saudi Arabia',
plan: 'Standard',
status: 'Suspended',
expiry: '15-06-2026'
},
{
id: '5',
code: 'ORG-0022',
organizationName: 'Oasis Foods',
countryId: 'c1',
country: 'Saudi Arabia',
plan: 'Standard',
status: 'Suspended',
expiry: '15-06-2026'
},
{
id: '6',
code: 'ORG-0023',
organizationName: 'Oasis Foods',
countryId: 'c1',
country: 'Saudi Arabia',
plan: 'Standard',
status: 'Suspended',
expiry: '15-06-2026'
},
{
id: '7',
code: 'ORG-0024',
organizationName: 'Oasis Foods',
countryId: 'c1',
country: 'Saudi Arabia',
plan: 'Standard',
status: 'Suspended',
expiry: '15-06-2026'
},
{
id: '8',
code: 'ORG-0025',
organizationName: 'Oasis Foods',
countryId: 'c1',
country: 'Saudi Arabia',
plan: 'Standard',
status: 'Suspended',
expiry: '15-06-2026'
},
{
id: '9',
code: 'ORG-0026',
organizationName: 'Oasis Foods',
countryId: 'c1',
country: 'Saudi Arabia',
plan: 'Standard',
status: 'Suspended',
expiry: '15-06-2026'
},
]);
readonly searchCountries: AutocompleteSearchFn<CountryLookupDto> = (term, limit) => this.countryApi.autocomplete(term, limit);
readonly displayCountry: AutocompleteDisplayFn<CountryLookupDto> = country => country.name;
readonly countryValue: AutocompleteValueFn<CountryLookupDto, string> = country => country.id;
readonly columns = signal<DataTableColumn<OrganizationListRow>[]>([
readonly columns = signal<DataTableColumn<OrganizationTableRow>[]>([
{ key: 'code', label: 'Code', header: 'Code', sortable: true },
{
key: 'organizationName',
@@ -173,7 +66,7 @@ export class OrganizationList implements OnInit {
sortable: true,
badge: true,
badgeClass: value =>
value === 'Trial'
value === 'Trial' || value === OrganizationStatus.Trial
? 'badge bg-warning/10 text-warning'
: 'badge bg-light text-defaulttextcolor',
},
@@ -184,15 +77,15 @@ export class OrganizationList implements OnInit {
sortable: true,
badge: true,
badgeClass: value => {
if (value === 'Active') return 'badge bg-success/10 text-success';
if (value === 'Trial') return 'badge bg-warning/10 text-warning';
if (value === 'Active' || value === OrganizationStatus.Active) return 'badge bg-success/10 text-success';
if (value === 'Trial' || value === OrganizationStatus.Trial) return 'badge bg-warning/10 text-warning';
return 'badge bg-danger/10 text-danger';
},
},
{ key: 'expiry', label: 'Expiry', header: 'Expiry', sortable: true },
]);
readonly actions = signal<DataTableAction<OrganizationListRow>[]>([
readonly actions = signal<DataTableAction<OrganizationTableRow>[]>([
{ type: 'view', label: 'View', icon: 'ti ti-eye', className: 'text-info' },
{ type: 'edit', label: 'Edit', icon: 'ti ti-edit', className: 'text-primary' },
{
@@ -200,52 +93,45 @@ export class OrganizationList implements OnInit {
label: 'Suspend',
icon: 'ti ti-player-pause',
className: 'text-warning',
visible: row => row.status !== 'Suspended',
visible: row => row.status !== 'Suspended' && row.status !== OrganizationStatus.Suspended,
},
]);
ngOnInit(): void {
this.tableStore.initialize({
fetcher: query => {
const countryId = this.appliedCountryId();
const selectedCountryName = this.selectedCountryLookup()?.name?.toLowerCase();
const search = (query.search || '').trim().toLowerCase();
const sortBy = query.sortBy;
const sortDir = query.sortDir;
fetcher: query => this.organizationApi.getOrganizationDataTable(query, this.appliedCountryId()),
mapRow: (item, serialNumber) => {
const statusText = typeof item.status === 'number'
? (OrganizationStatus[item.status] ?? 'Active')
: (item.status || 'Active');
let rows = this.allOrganizations().filter(row => {
const matchesCountry = !countryId
|| row.countryId === countryId
|| (!!selectedCountryName && row.country.toLowerCase() === selectedCountryName);
const matchesSearch = !search
|| row.code.toLowerCase().includes(search)
|| row.organizationName.toLowerCase().includes(search)
|| row.country.toLowerCase().includes(search)
|| row.plan.toLowerCase().includes(search)
|| row.status.toLowerCase().includes(search)
|| row.expiry.toLowerCase().includes(search);
return matchesCountry && matchesSearch;
});
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 (sortBy) {
rows = [...rows].sort((left, right) => {
const leftVal = String(left[sortBy as keyof OrganizationListRow] ?? '').toLowerCase();
const rightVal = String(right[sortBy as keyof OrganizationListRow] ?? '').toLowerCase();
const compared = leftVal.localeCompare(rightVal);
return sortDir === 'asc' ? compared : -compared;
});
}
const total = rows.length;
const start = (query.page - 1) * query.pageSize;
const end = start + query.pageSize;
const result: DataTableResult<OrganizationListRow> = {
draw: query.draw,
total,
filtered: total,
rows: rows.slice(start, end)
return {
...item,
id: item.id,
code: item.code || '',
name: resolvedName,
organizationName: resolvedName,
countryId: item.countryId || null,
country: resolvedCountry,
countryName: resolvedCountry,
plan: resolvedPlan,
planName: resolvedPlan,
status: statusText,
expiry: resolvedExpiry,
expiryDate: resolvedExpiry,
dataRegion: item.dataRegion || null,
isActive: item.isActive ?? true,
serialNumber,
};
return of(result);
},
onError: (err: any) => {
const msg = err?.error?.message || err?.error?.title || 'Failed to load organization list.';
this.toastr.error(msg);
}
});
}
@@ -279,7 +165,7 @@ export class OrganizationList implements OnInit {
this.showFilters.update(value => !value);
}
onActionClick(event: DataTableActionEvent<OrganizationListRow>): void {
onActionClick(event: DataTableActionEvent<OrganizationTableRow>): void {
if (event.action.type === 'edit' || (event.row.status as string) === 'Draft') {
void this.router.navigate(['/organizations/onboarding'], { queryParams: { id: event.row.id } });
return;
@@ -288,3 +174,4 @@ export class OrganizationList implements OnInit {
}
}
@@ -110,6 +110,25 @@
<!-- Button Group Footer -->
<footer class="modern-modal-footer mt-5 px-2 sm:px-6">
@if (isEditMode()) {
<div class="flex flex-col-reverse gap-2.5 sm:flex-row sm:items-center sm:justify-end sm:gap-3">
<!-- Cancel -->
<div class="w-full sm:w-auto">
<app-button action="cancel" [variant]="'custom'" label="Cancel" [showIcon]="true"
[disabled]="navigationDisabled()" [fullWidth]="true"
className="onboarding-nav-btn onboarding-save-btn ti-btn-outline-primary whitespace-nowrap justify-center sm:w-32"
(buttonClicked)="requestCancel()"></app-button>
</div>
<!-- Save Changes -->
<div class="w-full sm:w-auto">
<app-button action="update" label="Update" loadingLabel="Updating..." [showIcon]="true"
[loading]="finishing() || savingDraft()" [disabled]="navigationDisabled() || savingDraft() || finishing()"
[fullWidth]="true" className="onboarding-nav-btn justify-center sm:w-44"
(buttonClicked)="requestSaveChanges()"></app-button>
</div>
</div>
} @else {
<div class="flex flex-col-reverse gap-2.5 sm:flex-row sm:items-center sm:justify-end sm:gap-3">
<!-- Secondary Actions: Cancel/Back & Save Draft (Left group on Desktop, Bottom row on Mobile) -->
<div class="grid grid-cols-2 gap-2.5 w-full sm:flex sm:w-auto sm:items-center sm:gap-3">
@@ -153,6 +172,7 @@
}
</div>
</div>
}
</footer>
<!-- End Button Group Footer -->
</div>
@@ -24,6 +24,8 @@ export class OnboardingStepper {
readonly currentStepIndex = input.required<number>();
readonly completedStepIndexes = input<readonly number[]>([]);
readonly isEditMode = input(false);
readonly savingDraft = input(false);
readonly finishing = input(false);
readonly navigationDisabled = input(false);
@@ -32,6 +34,7 @@ export class OnboardingStepper {
readonly backClicked = output<void>();
readonly nextClicked = output<void>();
readonly saveDraftClicked = output<void>();
readonly saveChangesClicked = output<void>();
readonly finishClicked = output<void>();
readonly cancelClicked = output<void>();
@@ -122,6 +125,14 @@ export class OnboardingStepper {
this.saveDraftClicked.emit();
}
requestSaveChanges(): void {
if (this.navigationDisabled() || this.finishing() || this.savingDraft()) {
return;
}
this.saveChangesClicked.emit();
}
requestFinish(): void {
if (
this.navigationDisabled() ||
@@ -35,6 +35,7 @@ const INITIAL_DRAFT_STATE: OnboardingDraftState = {
@Injectable()
export class OrganizationOnboardingStateService {
private readonly isEditModeState = signal(false);
private readonly organizationIdState = signal<string | null>(null);
private readonly onboardingDataState = signal<OrganizationOnboardingData>(INITIAL_DATA);
private readonly onboardingDraftState = signal<OnboardingDraftState>(INITIAL_DRAFT_STATE);
@@ -43,6 +44,7 @@ export class OrganizationOnboardingStateService {
private readonly savedDraftIdState = signal<string | null>(null);
private readonly savedAtState = signal<string | null>(null);
readonly isEditMode = this.isEditModeState.asReadonly();
readonly organizationId = this.organizationIdState.asReadonly();
readonly onboardingData = this.onboardingDataState.asReadonly();
readonly onboardingDraft = this.onboardingDraftState.asReadonly();
@@ -60,6 +62,10 @@ export class OrganizationOnboardingStateService {
return !!(data.basics || data.localization || data.planLimits || data.admin);
});
setIsEditMode(isEdit: boolean): void {
this.isEditModeState.set(isEdit);
}
setOrganizationId(id: string | null): void {
this.organizationIdState.set(id);
}
@@ -164,28 +170,68 @@ export class OrganizationOnboardingStateService {
};
}
restoreServerDraft(serverDraft: OrganizationServerDraftResponse): void {
restoreServerDraft(serverDraft: OrganizationServerDraftResponse, isEditMode = false): void {
this.organizationIdState.set(serverDraft.id);
this.isEditModeState.set(isEditMode);
const rawRecord = serverDraft as Record<string, any>;
const orgCode = serverDraft.code ?? rawRecord['Code'] ?? rawRecord['code'] ?? rawRecord['organizationCode'] ?? rawRecord['OrganizationCode'] ?? null;
let basicsData = serverDraft.basics ?? rawRecord['Basics'] ?? rawRecord['basics'] ?? (
rawRecord['name'] || rawRecord['Name'] || rawRecord['organizationName'] || rawRecord['OrganizationName'] || orgCode
? rawRecord
: null
);
if (basicsData) {
basicsData = {
...basicsData,
code: basicsData['code'] ?? basicsData['Code'] ?? basicsData['organizationCode'] ?? basicsData['OrganizationCode'] ?? orgCode,
organizationCode: basicsData['organizationCode'] ?? basicsData['OrganizationCode'] ?? basicsData['code'] ?? basicsData['Code'] ?? orgCode,
};
}
const localizationData = serverDraft.localization ?? rawRecord['Localization'] ?? rawRecord['localization'] ?? (
rawRecord['defaultTimezoneId'] || rawRecord['DefaultTimezoneId'] || rawRecord['defaultCurrencyId'] || rawRecord['DefaultCurrencyId'] || rawRecord['dateFormat'] || rawRecord['DateFormat']
? rawRecord
: null
);
const planLimitsData = serverDraft.planLimits ?? rawRecord['PlanLimits'] ?? rawRecord['planLimits'] ?? rawRecord['plan'] ?? rawRecord['Plan'] ?? (
rawRecord['planId'] || rawRecord['PlanId'] || rawRecord['licenseType'] || rawRecord['LicenseType'] || rawRecord['maxCompanies'] || rawRecord['MaxCompanies']
? rawRecord
: null
);
const adminData = serverDraft.admin ?? rawRecord['Admin'] ?? rawRecord['admin'] ?? rawRecord['adminContact'] ?? rawRecord['AdminContact'] ?? (
rawRecord['adminEmail'] || rawRecord['AdminEmail'] || rawRecord['orgEmail'] || rawRecord['OrgEmail'] || rawRecord['administratorEmail'] || rawRecord['AdministratorEmail']
? rawRecord
: null
);
this.onboardingDraftState.set({
basics: serverDraft.basics ? { ...serverDraft.basics } : null,
localization: serverDraft.localization ? { ...serverDraft.localization } : null,
planLimits: serverDraft.planLimits ? { ...serverDraft.planLimits } : null,
admin: serverDraft.admin ? { ...serverDraft.admin } : null,
basics: basicsData ? { ...basicsData } : null,
localization: localizationData ? { ...localizationData } : null,
planLimits: planLimitsData ? { ...planLimitsData } : null,
admin: adminData ? { ...adminData } : null,
});
const completedIndexes = serverDraft.completedStepIndexes ?? [];
const completedIndexes = isEditMode
? [0, 1, 2, 3]
: (serverDraft.completedStepIndexes ?? []);
this.onboardingDataState.set({
basics: completedIndexes.includes(0) && serverDraft.basics
? { ...serverDraft.basics } as OrganizationBasicsValue
basics: (isEditMode || completedIndexes.includes(0)) && basicsData
? { ...basicsData } as OrganizationBasicsValue
: null,
localization: completedIndexes.includes(1) && serverDraft.localization
? { ...serverDraft.localization } as OrganizationLocalizationValue
localization: (isEditMode || completedIndexes.includes(1)) && localizationData
? { ...localizationData } as OrganizationLocalizationValue
: null,
planLimits: completedIndexes.includes(2) && serverDraft.planLimits
? { ...serverDraft.planLimits } as OrganizationPlanLimitsValue
planLimits: (isEditMode || completedIndexes.includes(2)) && planLimitsData
? { ...planLimitsData } as OrganizationPlanLimitsValue
: null,
admin: completedIndexes.includes(3) && serverDraft.admin
? { ...serverDraft.admin } as OrganizationAdminValue
admin: (isEditMode || completedIndexes.includes(3)) && adminData
? { ...adminData } as OrganizationAdminValue
: null,
});
@@ -227,6 +273,7 @@ export class OrganizationOnboardingStateService {
}
clear(): void {
this.isEditModeState.set(false);
this.organizationIdState.set(null);
this.onboardingDataState.set(INITIAL_DATA);
this.onboardingDraftState.set(INITIAL_DRAFT_STATE);
@@ -341,6 +341,9 @@ export function mapPlanStepToApiRequest(
export interface UpdateOrganizationAdminContactApiRequest {
AdminEmail?: string | null;
AdminFullName?: string | null;
AdminMobile?: string | null;
AdminPhone?: string | null;
AdministratorMobile?: string | null;
OrgEmail?: string | null;
OrgPhone?: string | null;
MarkComplete: boolean;
@@ -352,9 +355,14 @@ export function mapAdminContactStepToApiRequest(
admin: Partial<OrganizationAdminValue>,
markComplete: boolean
): UpdateOrganizationAdminContactApiRequest {
const mobile = admin.administratorMobile || (admin as any)?.adminMobile || (admin as any)?.AdminMobile || (admin as any)?.adminPhone || (admin as any)?.AdminPhone || null;
return {
AdminEmail: admin.administratorEmail || null,
AdminFullName: admin.administratorFullName || null,
AdminMobile: mobile,
AdminPhone: mobile,
AdministratorMobile: mobile,
OrgEmail: admin.organizationEmail || null,
OrgPhone: admin.organizationPhone || null,
MarkComplete: markComplete,
@@ -10,10 +10,12 @@
[savingDraft]="savingDraft()"
[finishing]="finishing()"
[navigationDisabled]="navigationDisabled()"
[isEditMode]="stateService.isEditMode()"
(stepSelected)="onStepSelected($event)"
(backClicked)="onBack()"
(nextClicked)="onNext()"
(saveDraftClicked)="onSaveDraft()"
(saveChangesClicked)="onUpdateCurrentStep()"
(finishClicked)="onFinish()"
(cancelClicked)="onCancel()"
>
@@ -10,7 +10,7 @@ import {
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { catchError, finalize, Observable, of, switchMap } from 'rxjs';
import { catchError, finalize, forkJoin, Observable, of, switchMap } from 'rxjs';
import {
OnboardingStep,
OnboardingStepper
@@ -87,18 +87,19 @@ export class OrganizationOnboarding {
const draftId = this.route.snapshot.queryParamMap.get('id');
if (draftId) {
this.stateService.setIsEditMode(true);
this.onboardingService.getOrganizationById(draftId)
.pipe(
catchError(err => {
const errorMsg = err?.error?.detail || err?.error?.message || 'Unable to restore draft from server.';
this.toastr.error(errorMsg, 'Draft Restore Failed');
const errorMsg = err?.error?.detail || err?.error?.message || 'Unable to restore organization details from server.';
this.toastr.error(errorMsg, 'Organization Restoration Failed');
return of(null);
}),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(draft => {
if (draft) {
this.stateService.restoreServerDraft(draft);
this.stateService.restoreServerDraft(draft, true);
this.savedDraftSnapshot = this.serializeDraftState();
this.hydrateActiveStep();
}
@@ -197,7 +198,12 @@ export class OrganizationOnboarding {
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.');
this.toastr.error('Complete and validate every onboarding step before saving.');
return;
}
if (this.stateService.isEditMode()) {
this.onUpdateCurrentStep();
return;
}
@@ -216,6 +222,87 @@ export class OrganizationOnboarding {
}
}
onUpdateCurrentStep(): void {
if (this.finishing() || this.savingDraft()) {
return;
}
const orgId = this.stateService.organizationId();
if (!orgId) {
this.toastr.error('Organization ID is missing. Unable to update step details.', 'Update Failed');
return;
}
const currentIndex = this.currentStepIndex();
const activeStepComponent = this.getStepComponent(currentIndex);
if (!activeStepComponent) {
return;
}
if (!activeStepComponent.validate()) {
const stepLabel = this.steps[currentIndex]?.label ?? `Step ${currentIndex + 1}`;
this.toastr.warning(
`Please fix the validation errors in ${stepLabel} before saving.`,
'Validation Required'
);
return;
}
this.storeValidatedStep(currentIndex, activeStepComponent);
this.finishing.set(true);
const currentStepLabel = this.steps[currentIndex]?.label ?? `Step ${currentIndex + 1}`;
let update$: Observable<unknown>;
switch (currentIndex) {
case 0: {
const data = (activeStepComponent as OrganizationBasicsStepComponent).getValue();
const payload = mapBasicsStepToApiRequest(data, true);
update$ = this.onboardingService.updateBasics(orgId, payload);
break;
}
case 1: {
const data = (activeStepComponent as OrganizationLocalizationStepComponent).getValue();
const payload = mapLocalizationStepToApiRequest(data, true);
update$ = 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);
break;
}
case 3: {
const data = (activeStepComponent as OrganizationAdminStepComponent).getValue();
const payload = mapAdminContactStepToApiRequest(data, true);
update$ = this.onboardingService.updateAdminContact(orgId, payload);
break;
}
default:
update$ = of(null);
break;
}
update$
.pipe(
finalize(() => this.finishing.set(false)),
takeUntilDestroyed(this.destroyRef)
)
.subscribe({
next: () => {
this.stateService.markStepCompleted(currentIndex);
this.savedDraftSnapshot = this.serializeDraftState();
this.toastr.success(`${currentStepLabel} details updated successfully.`, 'Changes Saved');
},
error: (err) => {
const errorMsg = err?.error?.detail || err?.error?.message || `Failed to update ${currentStepLabel.toLowerCase()} details.`;
this.toastr.error(errorMsg, 'Update Failed');
}
});
}
onCancel(): void {
this.captureActiveStepDraft();
if (this.serializeDraftState() !== this.savedDraftSnapshot) {
@@ -399,7 +486,6 @@ export class OrganizationOnboarding {
private captureActiveStepDraft(): void {
const currentIndex = this.currentStepIndex();
const validatedValue = this.getValidatedStepValue(currentIndex);
switch (currentIndex) {
case 0:
@@ -415,14 +501,6 @@ export class OrganizationOnboarding {
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 {
@@ -109,47 +109,37 @@
<div>
<dt class="text-xs text-textmuted">Organization</dt>
<dd class="font-medium text-defaulttextcolor dark:text-white">
{{ reviewData().basics?.organizationName || 'Not completed' }}
{{ organizationNameSummary() }}
</dd>
</div>
<div>
<dt class="text-xs text-textmuted">Country</dt>
<dd class="font-medium text-defaulttextcolor dark:text-white">
{{ reviewData().basics?.registrationCountry?.label || 'Not completed' }}
{{ countrySummary() }}
</dd>
</div>
<div>
<dt class="text-xs text-textmuted">Localization</dt>
<dd class="font-medium text-defaulttextcolor dark:text-white">
{{ reviewData().localization?.timeZone?.label || 'Not completed' }} ·
{{ reviewData().localization?.currency?.secondaryLabel || reviewData().localization?.currency?.label || '—' }}
{{ localizationSummary() }}
</dd>
</div>
<div>
<dt class="text-xs text-textmuted">Plan and limits</dt>
<dd class="font-medium text-defaulttextcolor dark:text-white">
{{ reviewData().planLimits?.subscriptionPlan?.label || 'Not completed' }}
@if (reviewData().planLimits) {
· {{ reviewData().planLimits?.maximumCompanies }} companies ·
{{ reviewData().planLimits?.maximumUsers }} users ·
{{ reviewData().planLimits?.maximumStorageGb }} GB
}
{{ planSummary() }}
</dd>
</div>
<div>
<dt class="text-xs text-textmuted">Access dates</dt>
<dd class="font-medium text-defaulttextcolor dark:text-white">
{{ reviewData().planLimits?.systemAccessStartDate || 'Not completed' }}
{{ reviewData().planLimits?.systemAccessEndDate || 'No end date' }}
{{ accessDatesSummary() }}
</dd>
</div>
<div>
<dt class="text-xs text-textmuted">Administrator</dt>
<dd class="font-medium text-defaulttextcolor dark:text-white">
{{ form.controls.administratorFullName.value || 'Not entered' }}
@if (form.controls.administratorEmail.value) {
· {{ form.controls.administratorEmail.value }}
}
{{ adminSummary() }}
</dd>
</div>
</dl>
@@ -2,10 +2,12 @@ import {
ChangeDetectionStrategy,
Component,
ElementRef,
computed,
inject,
input,
signal,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { FormInput } from '../../../../../shared/components/form/form-input/form-input';
@@ -45,6 +47,73 @@ export class OrganizationAdminStepComponent implements OnboardingStepForm<Organi
administratorMobile: ['', [Validators.required]],
});
private readonly formValues = toSignal(this.form.valueChanges, {
initialValue: this.form.getRawValue(),
});
readonly organizationNameSummary = computed(() => {
const basics = this.reviewData()?.basics as Record<string, any> | null;
return (
basics?.['organizationName'] ||
basics?.['name'] ||
basics?.['OrganizationName'] ||
basics?.['Name'] ||
'Not completed'
);
});
readonly countrySummary = computed(() => {
const basics = this.reviewData()?.basics as Record<string, any> | null;
const countryObj = basics?.['registrationCountry'];
const countryLabel = countryObj?.['label'] || countryObj?.['name'];
return (
countryLabel ||
basics?.['countryName'] ||
basics?.['CountryName'] ||
basics?.['country'] ||
basics?.['Country'] ||
'Not completed'
);
});
readonly localizationSummary = computed(() => {
const loc = this.reviewData()?.localization as Record<string, any> | null;
if (!loc) return 'Not completed';
const tz = loc['timeZone']?.label || loc['defaultTimezoneName'] || loc['DefaultTimezoneName'] || 'Not completed';
const currency = loc['currency']?.secondaryLabel || loc['currency']?.label || loc['defaultCurrencyName'] || loc['DefaultCurrencyName'] || '—';
return `${tz} · ${currency}`;
});
readonly planSummary = computed(() => {
const plan = this.reviewData()?.planLimits as Record<string, any> | null;
if (!plan) return 'Not completed';
const name = plan['subscriptionPlan']?.label || plan['planName'] || plan['PlanName'] || plan['plan'] || 'Not completed';
const maxComp = plan['maximumCompanies'] ?? plan['maxCompanies'] ?? plan['MaxCompanies'] ?? 0;
const maxUsers = plan['maximumUsers'] ?? plan['maxUsers'] ?? plan['MaxUsers'] ?? 0;
const maxStorage = plan['maximumStorageGb'] ?? plan['maxStorageGb'] ?? plan['MaxStorageGb'] ?? 0;
return `${name} · ${maxComp} companies · ${maxUsers} users · ${maxStorage} GB`;
});
readonly accessDatesSummary = computed(() => {
const plan = this.reviewData()?.planLimits as Record<string, any> | null;
if (!plan) return 'Not completed';
const start = plan['systemAccessStartDate'] || plan['systemAccessFrom'] || plan['SystemAccessFrom'] || plan['goLiveDate'] || 'Not completed';
const end = plan['systemAccessEndDate'] || plan['systemAccessTo'] || plan['SystemAccessTo'] || 'No end date';
return `${start} ${end}`;
});
readonly adminSummary = computed(() => {
const formVal = this.formValues();
const adminData = this.reviewData()?.admin as Record<string, any> | null;
const fullName = formVal.administratorFullName || adminData?.['administratorFullName'] || adminData?.['adminFullName'] || adminData?.['AdminFullName'] || '';
const email = formVal.administratorEmail || adminData?.['administratorEmail'] || adminData?.['adminEmail'] || adminData?.['AdminEmail'] || '';
const mobile = formVal.administratorMobile || adminData?.['administratorMobile'] || adminData?.['adminMobile'] || adminData?.['AdminMobile'] || adminData?.['adminPhone'] || adminData?.['AdminPhone'] || '';
const parts = [fullName, email, mobile].filter(Boolean);
return parts.length > 0 ? parts.join(' · ') : 'Not entered';
});
validate(): boolean {
this.submitAttempted.set(true);
if (this.form.valid) {
@@ -71,12 +140,28 @@ export class OrganizationAdminStepComponent implements OnboardingStepForm<Organi
}
patchValue(value: Partial<OrganizationAdminValue> & Record<string, any>): void {
if (!value) return;
this.form.patchValue({
organizationEmail: value.organizationEmail ?? value['OrgEmail'] ?? value['orgEmail'] ?? '',
organizationPhone: value.organizationPhone ?? value['OrgPhone'] ?? value['orgPhone'] ?? '',
administratorFullName: value.administratorFullName ?? value['AdminFullName'] ?? value['adminFullName'] ?? '',
administratorEmail: value.administratorEmail ?? value['AdminEmail'] ?? value['adminEmail'] ?? '',
administratorMobile: value.administratorMobile ?? value['AdminMobile'] ?? value['adminMobile'] ?? '',
organizationEmail: value.organizationEmail ?? value['OrgEmail'] ?? value['orgEmail'] ?? value['OrganizationEmail'] ?? '',
organizationPhone: value.organizationPhone ?? value['OrgPhone'] ?? value['orgPhone'] ?? value['OrganizationPhone'] ?? '',
administratorFullName: value.administratorFullName ?? value['AdminFullName'] ?? value['adminFullName'] ?? value['AdministratorFullName'] ?? '',
administratorEmail: value.administratorEmail ?? value['AdminEmail'] ?? value['adminEmail'] ?? value['AdministratorEmail'] ?? '',
administratorMobile:
value.administratorMobile ??
value['AdminMobile'] ??
value['adminMobile'] ??
value['AdministratorMobile'] ??
value['administratorMobile'] ??
value['AdminPhone'] ??
value['adminPhone'] ??
value['AdministratorPhone'] ??
value['administratorPhone'] ??
value['mobile'] ??
value['Mobile'] ??
value['phone'] ??
value['Phone'] ??
'',
}, { emitEvent: false });
this.submitAttempted.set(false);
}
@@ -1,4 +1,5 @@
import { ChangeDetectionStrategy, Component, ElementRef, inject, signal } from '@angular/core';
import { ChangeDetectionStrategy, Component, DestroyRef, ElementRef, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { ToastrService } from 'ngx-toastr';
import { catchError, map, of } from 'rxjs';
@@ -24,8 +25,10 @@ import {
OrganizationBasicsValue,
} from '../../models/organization-onboarding.model';
import { OrganizationOnboardingService } from '../../data-access/services/organization-onboarding.service';
import { OrganizationOnboardingStateService } from '../../data-access/services/organization-onboarding-state.service';
interface OrganizationBasicsFormModel {
readonly code: string | null;
readonly organizationName: string;
readonly shortName: string;
readonly localLanguageName: string;
@@ -42,6 +45,7 @@ interface OrganizationBasicsFormModel {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OrganizationBasicsStepComponent implements OnboardingStepForm<OrganizationBasicsValue> {
private readonly destroyRef = inject(DestroyRef);
private readonly formBuilder = inject(FormBuilder);
private readonly onboardingService = inject(OrganizationOnboardingService);
private readonly countryService = inject(CountryService);
@@ -49,6 +53,8 @@ export class OrganizationBasicsStepComponent implements OnboardingStepForm<Organ
private readonly toastr = inject(ToastrService);
private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
private readonly stateService = inject(OrganizationOnboardingStateService, { optional: true });
readonly submitAttempted = signal(false);
readonly organizationCodeControl = this.formBuilder.nonNullable.control('Pending generation');
readonly organizationTypeSelection = signal<OnboardingLookupValue | null>(null);
@@ -152,8 +158,11 @@ export class OrganizationBasicsStepComponent implements OnboardingStepForm<Organ
throw new Error('Organization basics selections are incomplete.');
}
const codeControlVal = this.organizationCodeControl.value;
const codeVal = codeControlVal && codeControlVal !== 'Pending generation' ? codeControlVal : null;
return {
organizationCode: null,
organizationCode: codeVal,
organizationName: value.organizationName.trim(),
shortName: value.shortName.trim(),
localLanguageName: value.localLanguageName.trim() || null,
@@ -169,45 +178,126 @@ export class OrganizationBasicsStepComponent implements OnboardingStepForm<Organ
getDraftValue(): Partial<OrganizationBasicsValue> {
const value = this.form.getRawValue() as OrganizationBasicsFormModel;
const country = this.registrationCountrySelection();
const codeControlVal = this.organizationCodeControl.value;
const codeVal = codeControlVal && codeControlVal !== 'Pending generation' ? codeControlVal : null;
return {
organizationCode: null,
organizationCode: codeVal,
organizationName: value.organizationName,
shortName: value.shortName,
localLanguageName: value.localLanguageName || null,
organizationType: this.organizationTypeSelection() ?? undefined,
industry: this.industrySelection() ?? undefined,
registrationCountry: this.registrationCountrySelection()
registrationCountry: country
? {
id: this.registrationCountrySelection()!.id,
label: this.registrationCountrySelection()!.name,
iso2: this.registrationCountrySelection()!.iso2,
}
id: country.id,
label: country.name,
name: country.name,
iso2: country.iso2,
} as any
: undefined,
};
}
patchValue(value: Partial<OrganizationBasicsValue>): void {
patchValue(value: Partial<OrganizationBasicsValue> & Record<string, any>): void {
if (!value) return;
const draftBasics = this.stateService?.onboardingDraft()?.basics as Record<string, any> | null;
const orgCode =
value.organizationCode ??
value['organizationCode'] ??
value['OrganizationCode'] ??
value['code'] ??
value['Code'] ??
draftBasics?.['organizationCode'] ??
draftBasics?.['code'] ??
draftBasics?.['Code'] ??
null;
if (orgCode) {
this.organizationCodeControl.setValue(String(orgCode));
}
const orgName = value.organizationName ?? value['OrganizationName'] ?? value['name'] ?? value['Name'] ?? '';
const shortName = value.shortName ?? value['ShortName'] ?? '';
const localLanguageName = value.localLanguageName ?? value['LocalLanguageName'] ?? '';
// 1. Organization Type
const rawOrgTypeId = value.organizationType?.id ?? value['organizationTypeId'] ?? value['OrganizationTypeId'] ?? value['orgType'] ?? value['OrgType'] ?? null;
const orgTypeParsedId = rawOrgTypeId != null ? String(rawOrgTypeId) : null;
const orgTypeLabel = value.organizationType?.label ?? value['organizationTypeName'] ?? value['OrganizationTypeName'] ?? '';
// 2. Industry
const rawIndustryId = value.industry?.id ?? value['industryId'] ?? value['IndustryId'] ?? null;
const industryId = rawIndustryId ? String(rawIndustryId) : null;
const industryLabel = value.industry?.label ?? value['industryName'] ?? value['IndustryName'] ?? '';
// 3. Country
const rawCountryId = value.registrationCountry?.id ?? (value.registrationCountry as any)?.id ?? value['registrationCountryId'] ?? value['RegistrationCountryId'] ?? value['countryId'] ?? value['CountryId'] ?? null;
const countryId = rawCountryId ? String(rawCountryId) : null;
const countryName = value.registrationCountry?.label ?? (value.registrationCountry as any)?.name ?? value['registrationCountryName'] ?? value['RegistrationCountryName'] ?? value['countryName'] ?? value['CountryName'] ?? '';
const countryIso2 = value.registrationCountry?.iso2 ?? value['registrationCountryIso2'] ?? value['RegistrationCountryIso2'] ?? value['countryIso2'] ?? value['CountryIso2'] ?? '';
this.form.patchValue({
organizationName: value.organizationName ?? '',
shortName: value.shortName ?? '',
localLanguageName: value.localLanguageName ?? '',
organizationTypeId: value.organizationType?.id ?? null,
industryId: value.industry?.id ?? null,
registrationCountryId: value.registrationCountry?.id ?? null,
organizationName: orgName,
shortName: shortName,
localLanguageName: localLanguageName,
organizationTypeId: orgTypeParsedId,
industryId: industryId,
registrationCountryId: countryId,
}, { emitEvent: false });
this.organizationTypeSelection.set(value.organizationType ?? null);
this.industrySelection.set(value.industry ?? null);
this.registrationCountrySelection.set(
value.registrationCountry
? {
id: value.registrationCountry.id,
iso2: value.registrationCountry.iso2 ?? '',
name: value.registrationCountry.label,
}
: null
);
// Handle Organization Type Selection
if (orgTypeParsedId != null) {
if (orgTypeLabel) {
this.organizationTypeSelection.set({ id: orgTypeParsedId, label: orgTypeLabel });
} else {
this.onboardingService.resolveOrganizationType(orgTypeParsedId).pipe(
catchError(() => of(null)),
takeUntilDestroyed(this.destroyRef)
).subscribe(resolved => {
if (resolved) this.organizationTypeSelection.set(resolved);
});
}
} else {
this.organizationTypeSelection.set(null);
}
// Handle Industry Selection
if (industryId) {
if (industryLabel) {
this.industrySelection.set({ id: industryId, label: industryLabel, secondaryLabel: value.industry?.secondaryLabel });
} else {
this.industryApiService.getIndustryById(industryId).pipe(
map(item => item ? ({ id: item.id, label: item.industryName, secondaryLabel: item.industryCode }) : null),
catchError(() => of(null)),
takeUntilDestroyed(this.destroyRef)
).subscribe(resolved => {
if (resolved) this.industrySelection.set(resolved);
});
}
} else {
this.industrySelection.set(null);
}
// Handle Country Selection
if (countryId) {
if (countryName) {
this.registrationCountrySelection.set({ id: countryId, iso2: countryIso2, name: countryName });
} else {
this.countryService.getCountryById(countryId).pipe(
map(country => ({ id: country.id, iso2: country.iso2, name: country.name })),
catchError(() => of(null)),
takeUntilDestroyed(this.destroyRef)
).subscribe(resolved => {
if (resolved) this.registrationCountrySelection.set(resolved);
});
}
} else {
this.registrationCountrySelection.set(null);
}
this.submitAttempted.set(false);
}
@@ -289,12 +289,20 @@ export class OrganizationLocalizationStepComponent implements OnboardingStepForm
}
patchValue(value: Partial<OrganizationLocalizationValue> & Record<string, any>): void {
if (!value) return;
const timeZoneId = value.timeZone?.id ?? value['DefaultTimezoneId'] ?? value['defaultTimezoneId'] ?? value['timeZoneId'] ?? null;
const currencyId = value.currency?.id ?? value['DefaultCurrencyId'] ?? value['defaultCurrencyId'] ?? value['currencyId'] ?? null;
const defaultLanguageId = value.defaultLanguage?.id ?? value['DefaultLanguageId'] ?? value['defaultLanguageId'] ?? value['languageId'] ?? null;
const additionalLanguageIds = value.additionalLanguageIds ?? value['AdditionalLanguageIds'] ?? value['additionalLanguageIds'] ?? [];
const dateFormat = value.dateFormat ?? value['DateFormat'] ?? value['dateFormat'] ?? null;
const rawTf = value.timeFormat !== undefined && value.timeFormat !== null ? value.timeFormat : value['TimeFormat'] ?? value['timeFormat'];
const timeFormatNormalized: TimeFormatValue | null =
rawTf === 1 || rawTf === '1' || rawTf === 'TwentyFourHour'
? 1
? 'TwentyFourHour'
: rawTf === 0 || rawTf === '0' || rawTf === 'TwelveHour'
? 0
? 'TwelveHour'
: null;
const rawFiscalYear = value.fiscalYearConvention ?? value['FiscalYearStart'] ?? value['fiscalYearStart'] ?? null;
@@ -304,46 +312,88 @@ export class OrganizationLocalizationStepComponent implements OnboardingStepForm
const numberFormatNormalized = mapApiToNumberFormat(rawNumberFormat);
this.form.patchValue({
timeZoneId: value.timeZone?.id ?? value['DefaultTimezoneId'] ?? value['defaultTimezoneId'] ?? value['timeZoneId'] ?? null,
currencyId: value.currency?.id ?? value['DefaultCurrencyId'] ?? value['defaultCurrencyId'] ?? value['currencyId'] ?? null,
defaultLanguageId: value.defaultLanguage?.id ?? value['DefaultLanguageId'] ?? value['defaultLanguageId'] ?? value['defaultLanguageId'] ?? null,
additionalLanguageIds: value.additionalLanguageIds ?? value['AdditionalLanguageIds'] ?? value['additionalLanguageIds'] ?? [],
dateFormat: value.dateFormat ?? value['DateFormat'] ?? value['dateFormat'] ?? null,
timeZoneId,
currencyId,
defaultLanguageId,
additionalLanguageIds,
dateFormat,
timeFormat: timeFormatNormalized,
numberFormat: numberFormatNormalized,
fiscalYearConvention: fiscalYearNormalized,
}, { emitEvent: false });
this.timeZoneSelection.set(
value.timeZone
? {
id: value.timeZone.id,
ianaId: value.timeZone.secondaryLabel ?? value.timeZone.label,
displayName: value.timeZone.label,
}
: null
);
this.currencySelection.set(
value.currency
? {
id: value.currency.id,
code: value.currency.secondaryLabel ?? value.currency.label,
name: value.currency.label,
symbol: '',
}
: null
);
this.defaultLanguageSelection.set(
value.defaultLanguage
? {
id: value.defaultLanguage.id,
code: value.defaultLanguage.secondaryLabel ?? value.defaultLanguage.label,
name: value.defaultLanguage.label,
nativeName: value.defaultLanguage.label,
isRightToLeft: false,
}
: null
);
// Timezone Selection
if (value.timeZone) {
this.timeZoneSelection.set({
id: value.timeZone.id,
ianaId: value.timeZone.secondaryLabel ?? value.timeZone.label,
displayName: value.timeZone.label,
});
} else if (timeZoneId) {
const tzName = value['DefaultTimezoneName'] ?? value['defaultTimezoneName'] ?? value['timeZoneName'] ?? '';
if (tzName) {
this.timeZoneSelection.set({ id: timeZoneId, ianaId: tzName, displayName: tzName });
} else {
this.timezoneService.getById(timeZoneId).pipe(
catchError(() => of(null)),
takeUntilDestroyed(this.destroyRef)
).subscribe(tz => {
if (tz) this.timeZoneSelection.set({ id: tz.id, ianaId: tz.ianaId, displayName: tz.displayName });
});
}
} else {
this.timeZoneSelection.set(null);
}
// Currency Selection
if (value.currency) {
this.currencySelection.set({
id: value.currency.id,
code: value.currency.secondaryLabel ?? value.currency.label,
name: value.currency.label,
symbol: '',
});
} else if (currencyId) {
const currName = value['DefaultCurrencyName'] ?? value['defaultCurrencyName'] ?? value['currencyName'] ?? '';
if (currName) {
this.currencySelection.set({ id: currencyId, code: currName, name: currName, symbol: '' });
} else {
this.currencyService.getCurrencyById(currencyId).pipe(
catchError(() => of(null)),
takeUntilDestroyed(this.destroyRef)
).subscribe(c => {
if (c) this.currencySelection.set({ id: c.id, code: c.code, name: c.name, symbol: c.symbol });
});
}
} else {
this.currencySelection.set(null);
}
// Default Language Selection
if (value.defaultLanguage) {
this.defaultLanguageSelection.set({
id: value.defaultLanguage.id,
code: value.defaultLanguage.secondaryLabel ?? value.defaultLanguage.label,
name: value.defaultLanguage.label,
nativeName: value.defaultLanguage.label,
isRightToLeft: false,
});
} else if (defaultLanguageId) {
const langName = value['DefaultLanguageName'] ?? value['defaultLanguageName'] ?? value['languageName'] ?? '';
if (langName) {
this.defaultLanguageSelection.set({ id: defaultLanguageId, code: langName, name: langName, nativeName: langName, isRightToLeft: false });
} else {
this.languageService.getById(defaultLanguageId).pipe(
catchError(() => of(null)),
takeUntilDestroyed(this.destroyRef)
).subscribe(l => {
if (l) this.defaultLanguageSelection.set({ id: l.id, code: l.code, name: l.name, nativeName: l.nativeName, isRightToLeft: l.isRightToLeft });
});
}
} else {
this.defaultLanguageSelection.set(null);
}
this.additionalLanguageOptions.update(current => this.mergeSelectionOptions(current, value.additionalLanguageSelections ?? []));
this.submitAttempted.set(false);
}
@@ -224,32 +224,47 @@ export class OrganizationPlanLimitsStepComponent implements OnboardingStepForm<O
}
patchValue(value: Partial<OrganizationPlanLimitsValue> & Record<string, any>): void {
const rawLicenseType = value.licenseType ?? value['LicenseType'];
if (!value) return;
const rawLicenseType = value.licenseType ?? value['LicenseType'] ?? value['licenseType'];
const normalizedLicenseType = mapApiToLicenseType(rawLicenseType);
const planId = value.subscriptionPlan?.id ?? value['PlanId'] ?? value['subscriptionPlanId'] ?? null;
const planId = value.subscriptionPlan?.id ?? value['PlanId'] ?? value['subscriptionPlanId'] ?? value['planId'] ?? null;
const subPlan = value.subscriptionPlan ?? (value['SubscriptionPlan'] ? {
id: value['SubscriptionPlan']?.id ?? planId,
label: value['SubscriptionPlan']?.name ?? value['SubscriptionPlan']?.label ?? '',
code: value['SubscriptionPlan']?.code ?? '',
} : planId ? { id: planId, label: '', code: '' } : null);
const subPlan = value.subscriptionPlan ?? (value['SubscriptionPlan'] || value['plan'] || value['Plan'] ? {
id: value['SubscriptionPlan']?.id ?? value['plan']?.id ?? value['Plan']?.id ?? planId,
label: value['SubscriptionPlan']?.name ?? value['SubscriptionPlan']?.label ?? value['planName'] ?? value['PlanName'] ?? value['plan']?.name ?? '',
code: value['SubscriptionPlan']?.code ?? value['plan']?.code ?? value['Plan']?.code ?? '',
} : planId ? { id: planId, label: value['PlanName'] ?? value['planName'] ?? '', code: '' } : null);
const from = value.systemAccessStartDate ?? value['SystemAccessFrom'] ?? null;
const to = value.systemAccessEndDate ?? value['SystemAccessTo'] ?? null;
const from = value.systemAccessStartDate ?? value['SystemAccessFrom'] ?? value['systemAccessFrom'] ?? value['systemAccessStartDate'] ?? null;
const to = value.systemAccessEndDate ?? value['SystemAccessTo'] ?? value['systemAccessTo'] ?? value['systemAccessEndDate'] ?? null;
const systemAccessPeriod: DateRangeValue | null = (from || to) ? { from, to } : null;
this.form.patchValue({
subscriptionPlanId: planId,
licenseType: normalizedLicenseType,
maximumCompanies: value.maximumCompanies ?? value['MaxCompanies'] ?? null,
maximumUsers: value.maximumUsers ?? value['MaxUsers'] ?? null,
maximumStorageGb: value.maximumStorageGb ?? value['MaxStorageGb'] ?? null,
goLiveDate: value.goLiveDate ?? value['GoLiveDate'] ?? null,
maximumCompanies: value.maximumCompanies ?? value['MaxCompanies'] ?? value['maxCompanies'] ?? null,
maximumUsers: value.maximumUsers ?? value['MaxUsers'] ?? value['maxUsers'] ?? null,
maximumStorageGb: value.maximumStorageGb ?? value['MaxStorageGb'] ?? value['maxStorageGb'] ?? null,
goLiveDate: value.goLiveDate ?? value['GoLiveDate'] ?? value['goLiveDate'] ?? null,
systemAccessPeriod,
}, { emitEvent: false });
this.planSelection.set(subPlan ?? value.subscriptionPlan ?? null);
if (subPlan && subPlan.id) {
this.planSelection.set(subPlan);
if (!subPlan.label && planId) {
this.onboardingService.resolveSubscriptionPlan(planId).pipe(
catchError(() => of(null)),
takeUntilDestroyed(this.destroyRef)
).subscribe(plan => {
if (plan) this.planSelection.set(plan);
});
}
} else {
this.planSelection.set(null);
}
this.limitsEditable.set(value.limitsEditable ?? true);
this.applyLimitsEditableState();
this.submitAttempted.set(false);
@@ -265,10 +265,6 @@ export class TenantCurrencies {
return;
}
if (response.draw !== query.draw) {
return;
}
const rows: TenantCurrencyTableRow[] =
response.rows.map(
(tenantCurrency, index) => ({
@@ -2,7 +2,7 @@
[columns]="columns()"
[rows]="tableStore.rows()"
[actions]="actions()"
[totalRecords]="tableStore.totalRecords()"
[totalRecords]="tableStore.filteredRecords()"
[pageIndex]="tableStore.queryState.pageIndex()"
[pageSize]="tableStore.queryState.pageSize()"
tableTitle="Tenants"
+3 -1
View File
@@ -84,7 +84,9 @@
solid button ('Next') to outline style and remove its glow/box-shadow.
========================================================================== */
:host-context(.modern-modal-footer:has(.ti-btn-outline-primary:hover:not(:disabled))) button.ti-btn-primary-full:not(:hover),
:host-context(.modern-modal-footer:has(.onboarding-save-btn:hover:not(:disabled))) button.ti-btn-primary-full:not(:hover) {
:host-context(.modern-modal-footer:has(.ti-btn-outline-primary:hover:not(:disabled))) button.ti-btn-success-full:not(:hover),
:host-context(.modern-modal-footer:has(.onboarding-save-btn:hover:not(:disabled))) button.ti-btn-primary-full:not(:hover),
:host-context(.modern-modal-footer:has(.onboarding-save-btn:hover:not(:disabled))) button.ti-btn-success-full:not(:hover) {
background: #ffffff !important;
color: var(--primary, #7c3deb) !important;
border: 1.5px solid var(--primary, #7c3deb) !important;
@@ -114,7 +114,7 @@
</tr>
} @else
{
@for (row of rows(); track row['id']; let rowIndex = $index) {
@for (row of rows(); track row['id'] ?? $index; let rowIndex = $index) {
<tr [class]="getRowClass(row, $index)" (click)="onRowClick(row)">
@for (column of columns(); track column.key) {
<td [class]="getCellClass(column)">
@@ -58,9 +58,8 @@ export class DataTableStore<TItem, TRow extends DataTableRecord = TItem & DataTa
if (!response) return;
const currentQuery = this.queryState.getQuery();
if (response.draw !== currentQuery.draw) return;
const mappedRows: TRow[] = response.rows.map((item, index) => {
const rowsList = response.rows || [];
const mappedRows: TRow[] = rowsList.map((item, index) => {
const serialNumber = (currentQuery.page - 1) * currentQuery.pageSize + index + 1;
if (options.mapRow) {
return options.mapRow(item, serialNumber, currentQuery);
@@ -72,8 +71,8 @@ export class DataTableStore<TItem, TRow extends DataTableRecord = TItem & DataTa
});
this.rows.set(mappedRows);
this.totalRecords.set(response.total);
this.filteredRecords.set(response.filtered);
this.totalRecords.set(response.total ?? 0);
this.filteredRecords.set(response.filtered ?? response.total ?? 0);
});
this.load(this.queryState.getQuery());