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()" [columns]="columns()"
[rows]="tableStore.rows()" [rows]="tableStore.rows()"
[actions]="actions()" [actions]="actions()"
[totalRecords]="tableStore.totalRecords()" [totalRecords]="tableStore.filteredRecords()"
[pageIndex]="tableStore.queryState.pageIndex()" [pageIndex]="tableStore.queryState.pageIndex()"
[pageSize]="tableStore.queryState.pageSize()" [pageSize]="tableStore.queryState.pageSize()"
tableTitle="Recently Created Organizations" tableTitle="Recently Created Organizations"
@@ -1,7 +1,6 @@
import { Component, OnInit, inject, signal } from '@angular/core'; import { Component, OnInit, inject, signal } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { of } from 'rxjs';
import { DataTable } from '../../../shared/components/data-table/data-table'; import { DataTable } from '../../../shared/components/data-table/data-table';
import { DataTableStore } from '../../../shared/components/data-table/data-table.store'; import { DataTableStore } from '../../../shared/components/data-table/data-table.store';
@@ -9,12 +8,9 @@ import {
DataTableAction, DataTableAction,
DataTableActionEvent, DataTableActionEvent,
DataTableColumn, DataTableColumn,
DataTableRecord,
DataTableResult,
} from '../../../shared/components/data-table/data-table.types'; } from '../../../shared/components/data-table/data-table.types';
import { OrganizationDto, OrganizationStatus, OrganizationTableRow } from '../organization-list/models/organization.model';
type OrganizationPlan = 'Enterprise' | 'Standard' | 'Trial'; import { OrganizationService } from '../organization-list/data-access/organization.service';
type OrganizationStatus = 'Active' | 'Trial' | 'Suspended';
interface DashboardStatCard { interface DashboardStatCard {
title: string; title: string;
@@ -26,17 +22,6 @@ interface DashboardStatCard {
helperClass: string; helperClass: string;
} }
interface OrganizationListRow extends DataTableRecord {
id: string;
code: string;
organizationName: string;
countryId: string;
country: string;
plan: string;
status: OrganizationStatus;
expiry: string;
}
@Component({ @Component({
selector: 'dashboard', selector: 'dashboard',
standalone: true, standalone: true,
@@ -48,7 +33,8 @@ interface OrganizationListRow extends DataTableRecord {
export class Dashboard implements OnInit { export class Dashboard implements OnInit {
private readonly router = inject(Router); private readonly router = inject(Router);
private readonly toastr = inject(ToastrService); 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[]>([ readonly statCards = signal<DashboardStatCard[]>([
{ {
@@ -107,100 +93,8 @@ export class Dashboard implements OnInit {
}, },
]); ]);
readonly recentOrganizations = signal<OrganizationListRow[]>([ readonly columns = signal<DataTableColumn<OrganizationTableRow>[]>([
{ { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '90px' },
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>[]>([
{ key: 'code', label: 'Code', header: 'Code', sortable: true }, { key: 'code', label: 'Code', header: 'Code', sortable: true },
{ {
key: 'organizationName', key: 'organizationName',
@@ -217,7 +111,7 @@ export class Dashboard implements OnInit {
sortable: true, sortable: true,
badge: true, badge: true,
badgeClass: value => badgeClass: value =>
value === 'Trial' value === 'Trial' || value === OrganizationStatus.Trial
? 'badge bg-warning/10 text-warning' ? 'badge bg-warning/10 text-warning'
: 'badge bg-light text-defaulttextcolor', : 'badge bg-light text-defaulttextcolor',
}, },
@@ -228,9 +122,9 @@ export class Dashboard implements OnInit {
sortable: true, sortable: true,
badge: true, badge: true,
badgeClass: value => badgeClass: value =>
value === 'Active' value === 'Active' || value === OrganizationStatus.Active
? 'badge bg-success/10 text-success' ? 'badge bg-success/10 text-success'
: value === 'Trial' : value === 'Trial' || value === OrganizationStatus.Trial
? 'badge bg-warning/10 text-warning' ? 'badge bg-warning/10 text-warning'
: 'badge bg-danger/10 text-danger', : 'badge bg-danger/10 text-danger',
}, },
@@ -240,7 +134,7 @@ export class Dashboard implements OnInit {
readonly emptyMessage = signal<string>('No Organizations'); readonly emptyMessage = signal<string>('No Organizations');
readonly emptyDescription = signal<string>('Start by adding your first organization'); 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: 'view', label: 'View', icon: 'ti ti-eye', className: 'text-info' },
{ type: 'edit', label: 'Edit', icon: 'ti ti-edit', className: 'text-primary' }, { type: 'edit', label: 'Edit', icon: 'ti ti-edit', className: 'text-primary' },
{ {
@@ -248,47 +142,45 @@ export class Dashboard implements OnInit {
label: 'Suspend', label: 'Suspend',
icon: 'ti ti-player-pause', icon: 'ti ti-player-pause',
className: 'text-warning', className: 'text-warning',
visible: row => row.status !== 'Suspended', visible: row => row.status !== 'Suspended' && row.status !== OrganizationStatus.Suspended,
}, },
]); ]);
ngOnInit(): void { ngOnInit(): void {
this.tableStore.initialize({ this.tableStore.initialize({
fetcher: query => { fetcher: query => this.organizationApi.getOrganizationDataTable(query),
const search = (query.search || '').trim().toLowerCase(); mapRow: (item, serialNumber) => {
const sortBy = query.sortBy; const statusText = typeof item.status === 'number'
const sortDir = query.sortDir; ? (OrganizationStatus[item.status] ?? 'Active')
: (item.status || 'Active');
let rows = this.recentOrganizations().filter(row => { const resolvedName = item.name || item.organizationName || '—';
const matchesSearch = !search const resolvedCountry = item.countryName || item.country || '—';
|| row.code.toLowerCase().includes(search) const resolvedPlan = item.planName || item.plan || '—';
|| row.organizationName.toLowerCase().includes(search) const resolvedExpiry = item.expiryDate || item.expiry || '—';
|| row.country.toLowerCase().includes(search)
|| row.plan.toLowerCase().includes(search)
|| row.status.toLowerCase().includes(search)
|| (row.expiry && row.expiry.toLowerCase().includes(search));
return matchesSearch;
});
if (sortBy) { return {
rows = [...rows].sort((left, right) => { ...item,
const leftVal = String(left[sortBy as keyof OrganizationListRow] ?? '').toLowerCase(); id: item.id,
const rightVal = String(right[sortBy as keyof OrganizationListRow] ?? '').toLowerCase(); code: item.code || '',
const compared = leftVal.localeCompare(rightVal); name: resolvedName,
return sortDir === 'asc' ? compared : -compared; organizationName: resolvedName,
}); countryId: item.countryId || null,
} country: resolvedCountry,
countryName: resolvedCountry,
const total = rows.length; plan: resolvedPlan,
const start = (query.page - 1) * query.pageSize; planName: resolvedPlan,
const end = start + query.pageSize; status: statusText,
const result: DataTableResult<OrganizationListRow> = { expiry: resolvedExpiry,
draw: query.draw, expiryDate: resolvedExpiry,
total, dataRegion: item.dataRegion || null,
filtered: total, isActive: item.isActive ?? true,
rows: rows.slice(start, end) 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']); 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') { if (event.action.type === 'edit' || (event.row.status as string) === 'Draft') {
void this.router.navigate(['/organizations/onboarding'], { queryParams: { id: event.row.id } }); void this.router.navigate(['/organizations/onboarding'], { queryParams: { id: event.row.id } });
return; 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()" [columns]="columns()"
[rows]="tableStore.rows()" [rows]="tableStore.rows()"
[actions]="actions()" [actions]="actions()"
[totalRecords]="tableStore.totalRecords()" [totalRecords]="tableStore.filteredRecords()"
[pageIndex]="tableStore.queryState.pageIndex()" [pageIndex]="tableStore.queryState.pageIndex()"
[pageSize]="tableStore.queryState.pageSize()" [pageSize]="tableStore.queryState.pageSize()"
tableTitle="Organizations" tableTitle="Organizations"
@@ -2,7 +2,6 @@ import { Component, OnInit, inject, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { of } from 'rxjs';
import { DataTable, DataTableToolbarDirective } from '../../../shared/components/data-table/data-table'; import { DataTable, DataTableToolbarDirective } from '../../../shared/components/data-table/data-table';
import { DataTableStore } from '../../../shared/components/data-table/data-table.store'; import { DataTableStore } from '../../../shared/components/data-table/data-table.store';
@@ -10,31 +9,17 @@ import {
DataTableAction, DataTableAction,
DataTableActionEvent, DataTableActionEvent,
DataTableColumn, DataTableColumn,
DataTableRecord,
DataTableResult
} from '../../../shared/components/data-table/data-table.types'; } from '../../../shared/components/data-table/data-table.types';
import { Autocomplete } from '../../../shared/components/form/autocomplete/autocomplete'; import { Autocomplete } from '../../../shared/components/form/autocomplete/autocomplete';
import { import {
AutocompleteDisplayFn, AutocompleteDisplayFn,
AutocompleteResolveValueFn,
AutocompleteSearchFn, AutocompleteSearchFn,
AutocompleteValueFn, AutocompleteValueFn,
} from '../../../shared/components/form/autocomplete/autocomplete.types'; } from '../../../shared/components/form/autocomplete/autocomplete.types';
import { Button } from '../../../shared/components/button/button'; import { Button } from '../../../shared/components/button/button';
import { CountryLookupDto, CountryService } from '../../global-masters/countries/public-api'; import { CountryLookupDto, CountryService } from '../../global-masters/countries/public-api';
import { OrganizationDto, OrganizationStatus, OrganizationTableRow } from './models/organization.model';
type OrganizationStatus = 'Active' | 'Trial' | 'Suspended'; import { OrganizationService } from './data-access/organization.service';
interface OrganizationListRow extends DataTableRecord {
id: string;
code: string;
organizationName: string;
countryId: string;
country: string;
plan: string;
status: OrganizationStatus;
expiry: string;
}
@Component({ @Component({
selector: 'organization-list', selector: 'organization-list',
@@ -49,7 +34,8 @@ export class OrganizationList implements OnInit {
private readonly router = inject(Router); private readonly router = inject(Router);
private readonly toastr = inject(ToastrService); private readonly toastr = inject(ToastrService);
private readonly countryApi = inject(CountryService); 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 selectedCountryLookup = signal<CountryLookupDto | null>(null);
readonly appliedCountryId = signal<string | null>(null); readonly appliedCountryId = signal<string | null>(null);
@@ -59,104 +45,11 @@ export class OrganizationList implements OnInit {
countryId: [''], 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 searchCountries: AutocompleteSearchFn<CountryLookupDto> = (term, limit) => this.countryApi.autocomplete(term, limit);
readonly displayCountry: AutocompleteDisplayFn<CountryLookupDto> = country => country.name; readonly displayCountry: AutocompleteDisplayFn<CountryLookupDto> = country => country.name;
readonly countryValue: AutocompleteValueFn<CountryLookupDto, string> = country => country.id; 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: 'code', label: 'Code', header: 'Code', sortable: true },
{ {
key: 'organizationName', key: 'organizationName',
@@ -173,7 +66,7 @@ export class OrganizationList implements OnInit {
sortable: true, sortable: true,
badge: true, badge: true,
badgeClass: value => badgeClass: value =>
value === 'Trial' value === 'Trial' || value === OrganizationStatus.Trial
? 'badge bg-warning/10 text-warning' ? 'badge bg-warning/10 text-warning'
: 'badge bg-light text-defaulttextcolor', : 'badge bg-light text-defaulttextcolor',
}, },
@@ -184,15 +77,15 @@ export class OrganizationList implements OnInit {
sortable: true, sortable: true,
badge: true, badge: true,
badgeClass: value => { badgeClass: value => {
if (value === 'Active') return 'badge bg-success/10 text-success'; if (value === 'Active' || value === OrganizationStatus.Active) return 'badge bg-success/10 text-success';
if (value === 'Trial') return 'badge bg-warning/10 text-warning'; if (value === 'Trial' || value === OrganizationStatus.Trial) return 'badge bg-warning/10 text-warning';
return 'badge bg-danger/10 text-danger'; return 'badge bg-danger/10 text-danger';
}, },
}, },
{ key: 'expiry', label: 'Expiry', header: 'Expiry', sortable: true }, { 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: 'view', label: 'View', icon: 'ti ti-eye', className: 'text-info' },
{ type: 'edit', label: 'Edit', icon: 'ti ti-edit', className: 'text-primary' }, { type: 'edit', label: 'Edit', icon: 'ti ti-edit', className: 'text-primary' },
{ {
@@ -200,52 +93,45 @@ export class OrganizationList implements OnInit {
label: 'Suspend', label: 'Suspend',
icon: 'ti ti-player-pause', icon: 'ti ti-player-pause',
className: 'text-warning', className: 'text-warning',
visible: row => row.status !== 'Suspended', visible: row => row.status !== 'Suspended' && row.status !== OrganizationStatus.Suspended,
}, },
]); ]);
ngOnInit(): void { ngOnInit(): void {
this.tableStore.initialize({ this.tableStore.initialize({
fetcher: query => { fetcher: query => this.organizationApi.getOrganizationDataTable(query, this.appliedCountryId()),
const countryId = this.appliedCountryId(); mapRow: (item, serialNumber) => {
const selectedCountryName = this.selectedCountryLookup()?.name?.toLowerCase(); const statusText = typeof item.status === 'number'
const search = (query.search || '').trim().toLowerCase(); ? (OrganizationStatus[item.status] ?? 'Active')
const sortBy = query.sortBy; : (item.status || 'Active');
const sortDir = query.sortDir;
let rows = this.allOrganizations().filter(row => { const resolvedName = item.name || item.organizationName || '—';
const matchesCountry = !countryId const resolvedCountry = item.countryName || item.country || '—';
|| row.countryId === countryId const resolvedPlan = item.planName || item.plan || '—';
|| (!!selectedCountryName && row.country.toLowerCase() === selectedCountryName); const resolvedExpiry = item.expiryDate || item.expiry || '—';
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;
});
if (sortBy) { return {
rows = [...rows].sort((left, right) => { ...item,
const leftVal = String(left[sortBy as keyof OrganizationListRow] ?? '').toLowerCase(); id: item.id,
const rightVal = String(right[sortBy as keyof OrganizationListRow] ?? '').toLowerCase(); code: item.code || '',
const compared = leftVal.localeCompare(rightVal); name: resolvedName,
return sortDir === 'asc' ? compared : -compared; organizationName: resolvedName,
}); countryId: item.countryId || null,
} country: resolvedCountry,
countryName: resolvedCountry,
const total = rows.length; plan: resolvedPlan,
const start = (query.page - 1) * query.pageSize; planName: resolvedPlan,
const end = start + query.pageSize; status: statusText,
const result: DataTableResult<OrganizationListRow> = { expiry: resolvedExpiry,
draw: query.draw, expiryDate: resolvedExpiry,
total, dataRegion: item.dataRegion || null,
filtered: total, isActive: item.isActive ?? true,
rows: rows.slice(start, end) 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); 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') { if (event.action.type === 'edit' || (event.row.status as string) === 'Draft') {
void this.router.navigate(['/organizations/onboarding'], { queryParams: { id: event.row.id } }); void this.router.navigate(['/organizations/onboarding'], { queryParams: { id: event.row.id } });
return; return;
@@ -288,3 +174,4 @@ export class OrganizationList implements OnInit {
} }
} }
@@ -110,6 +110,25 @@
<!-- Button Group Footer --> <!-- Button Group Footer -->
<footer class="modern-modal-footer mt-5 px-2 sm:px-6"> <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"> <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) --> <!-- 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"> <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>
</div> </div>
}
</footer> </footer>
<!-- End Button Group Footer --> <!-- End Button Group Footer -->
</div> </div>
@@ -24,6 +24,8 @@ export class OnboardingStepper {
readonly currentStepIndex = input.required<number>(); readonly currentStepIndex = input.required<number>();
readonly completedStepIndexes = input<readonly number[]>([]); readonly completedStepIndexes = input<readonly number[]>([]);
readonly isEditMode = input(false);
readonly savingDraft = input(false); readonly savingDraft = input(false);
readonly finishing = input(false); readonly finishing = input(false);
readonly navigationDisabled = input(false); readonly navigationDisabled = input(false);
@@ -32,6 +34,7 @@ export class OnboardingStepper {
readonly backClicked = output<void>(); readonly backClicked = output<void>();
readonly nextClicked = output<void>(); readonly nextClicked = output<void>();
readonly saveDraftClicked = output<void>(); readonly saveDraftClicked = output<void>();
readonly saveChangesClicked = output<void>();
readonly finishClicked = output<void>(); readonly finishClicked = output<void>();
readonly cancelClicked = output<void>(); readonly cancelClicked = output<void>();
@@ -122,6 +125,14 @@ export class OnboardingStepper {
this.saveDraftClicked.emit(); this.saveDraftClicked.emit();
} }
requestSaveChanges(): void {
if (this.navigationDisabled() || this.finishing() || this.savingDraft()) {
return;
}
this.saveChangesClicked.emit();
}
requestFinish(): void { requestFinish(): void {
if ( if (
this.navigationDisabled() || this.navigationDisabled() ||
@@ -35,6 +35,7 @@ const INITIAL_DRAFT_STATE: OnboardingDraftState = {
@Injectable() @Injectable()
export class OrganizationOnboardingStateService { export class OrganizationOnboardingStateService {
private readonly isEditModeState = signal(false);
private readonly organizationIdState = signal<string | null>(null); private readonly organizationIdState = signal<string | null>(null);
private readonly onboardingDataState = signal<OrganizationOnboardingData>(INITIAL_DATA); private readonly onboardingDataState = signal<OrganizationOnboardingData>(INITIAL_DATA);
private readonly onboardingDraftState = signal<OnboardingDraftState>(INITIAL_DRAFT_STATE); private readonly onboardingDraftState = signal<OnboardingDraftState>(INITIAL_DRAFT_STATE);
@@ -43,6 +44,7 @@ export class OrganizationOnboardingStateService {
private readonly savedDraftIdState = signal<string | null>(null); private readonly savedDraftIdState = signal<string | null>(null);
private readonly savedAtState = signal<string | null>(null); private readonly savedAtState = signal<string | null>(null);
readonly isEditMode = this.isEditModeState.asReadonly();
readonly organizationId = this.organizationIdState.asReadonly(); readonly organizationId = this.organizationIdState.asReadonly();
readonly onboardingData = this.onboardingDataState.asReadonly(); readonly onboardingData = this.onboardingDataState.asReadonly();
readonly onboardingDraft = this.onboardingDraftState.asReadonly(); readonly onboardingDraft = this.onboardingDraftState.asReadonly();
@@ -60,6 +62,10 @@ export class OrganizationOnboardingStateService {
return !!(data.basics || data.localization || data.planLimits || data.admin); return !!(data.basics || data.localization || data.planLimits || data.admin);
}); });
setIsEditMode(isEdit: boolean): void {
this.isEditModeState.set(isEdit);
}
setOrganizationId(id: string | null): void { setOrganizationId(id: string | null): void {
this.organizationIdState.set(id); 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.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({ this.onboardingDraftState.set({
basics: serverDraft.basics ? { ...serverDraft.basics } : null, basics: basicsData ? { ...basicsData } : null,
localization: serverDraft.localization ? { ...serverDraft.localization } : null, localization: localizationData ? { ...localizationData } : null,
planLimits: serverDraft.planLimits ? { ...serverDraft.planLimits } : null, planLimits: planLimitsData ? { ...planLimitsData } : null,
admin: serverDraft.admin ? { ...serverDraft.admin } : null, admin: adminData ? { ...adminData } : null,
}); });
const completedIndexes = serverDraft.completedStepIndexes ?? []; const completedIndexes = isEditMode
? [0, 1, 2, 3]
: (serverDraft.completedStepIndexes ?? []);
this.onboardingDataState.set({ this.onboardingDataState.set({
basics: completedIndexes.includes(0) && serverDraft.basics basics: (isEditMode || completedIndexes.includes(0)) && basicsData
? { ...serverDraft.basics } as OrganizationBasicsValue ? { ...basicsData } as OrganizationBasicsValue
: null, : null,
localization: completedIndexes.includes(1) && serverDraft.localization localization: (isEditMode || completedIndexes.includes(1)) && localizationData
? { ...serverDraft.localization } as OrganizationLocalizationValue ? { ...localizationData } as OrganizationLocalizationValue
: null, : null,
planLimits: completedIndexes.includes(2) && serverDraft.planLimits planLimits: (isEditMode || completedIndexes.includes(2)) && planLimitsData
? { ...serverDraft.planLimits } as OrganizationPlanLimitsValue ? { ...planLimitsData } as OrganizationPlanLimitsValue
: null, : null,
admin: completedIndexes.includes(3) && serverDraft.admin admin: (isEditMode || completedIndexes.includes(3)) && adminData
? { ...serverDraft.admin } as OrganizationAdminValue ? { ...adminData } as OrganizationAdminValue
: null, : null,
}); });
@@ -227,6 +273,7 @@ export class OrganizationOnboardingStateService {
} }
clear(): void { clear(): void {
this.isEditModeState.set(false);
this.organizationIdState.set(null); this.organizationIdState.set(null);
this.onboardingDataState.set(INITIAL_DATA); this.onboardingDataState.set(INITIAL_DATA);
this.onboardingDraftState.set(INITIAL_DRAFT_STATE); this.onboardingDraftState.set(INITIAL_DRAFT_STATE);
@@ -341,6 +341,9 @@ export function mapPlanStepToApiRequest(
export interface UpdateOrganizationAdminContactApiRequest { export interface UpdateOrganizationAdminContactApiRequest {
AdminEmail?: string | null; AdminEmail?: string | null;
AdminFullName?: string | null; AdminFullName?: string | null;
AdminMobile?: string | null;
AdminPhone?: string | null;
AdministratorMobile?: string | null;
OrgEmail?: string | null; OrgEmail?: string | null;
OrgPhone?: string | null; OrgPhone?: string | null;
MarkComplete: boolean; MarkComplete: boolean;
@@ -352,9 +355,14 @@ export function mapAdminContactStepToApiRequest(
admin: Partial<OrganizationAdminValue>, admin: Partial<OrganizationAdminValue>,
markComplete: boolean markComplete: boolean
): UpdateOrganizationAdminContactApiRequest { ): UpdateOrganizationAdminContactApiRequest {
const mobile = admin.administratorMobile || (admin as any)?.adminMobile || (admin as any)?.AdminMobile || (admin as any)?.adminPhone || (admin as any)?.AdminPhone || null;
return { return {
AdminEmail: admin.administratorEmail || null, AdminEmail: admin.administratorEmail || null,
AdminFullName: admin.administratorFullName || null, AdminFullName: admin.administratorFullName || null,
AdminMobile: mobile,
AdminPhone: mobile,
AdministratorMobile: mobile,
OrgEmail: admin.organizationEmail || null, OrgEmail: admin.organizationEmail || null,
OrgPhone: admin.organizationPhone || null, OrgPhone: admin.organizationPhone || null,
MarkComplete: markComplete, MarkComplete: markComplete,
@@ -10,10 +10,12 @@
[savingDraft]="savingDraft()" [savingDraft]="savingDraft()"
[finishing]="finishing()" [finishing]="finishing()"
[navigationDisabled]="navigationDisabled()" [navigationDisabled]="navigationDisabled()"
[isEditMode]="stateService.isEditMode()"
(stepSelected)="onStepSelected($event)" (stepSelected)="onStepSelected($event)"
(backClicked)="onBack()" (backClicked)="onBack()"
(nextClicked)="onNext()" (nextClicked)="onNext()"
(saveDraftClicked)="onSaveDraft()" (saveDraftClicked)="onSaveDraft()"
(saveChangesClicked)="onUpdateCurrentStep()"
(finishClicked)="onFinish()" (finishClicked)="onFinish()"
(cancelClicked)="onCancel()" (cancelClicked)="onCancel()"
> >
@@ -10,7 +10,7 @@ import {
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { catchError, finalize, Observable, of, switchMap } from 'rxjs'; import { catchError, finalize, forkJoin, Observable, of, switchMap } from 'rxjs';
import { import {
OnboardingStep, OnboardingStep,
OnboardingStepper OnboardingStepper
@@ -87,18 +87,19 @@ export class OrganizationOnboarding {
const draftId = this.route.snapshot.queryParamMap.get('id'); const draftId = this.route.snapshot.queryParamMap.get('id');
if (draftId) { if (draftId) {
this.stateService.setIsEditMode(true);
this.onboardingService.getOrganizationById(draftId) this.onboardingService.getOrganizationById(draftId)
.pipe( .pipe(
catchError(err => { catchError(err => {
const errorMsg = err?.error?.detail || err?.error?.message || 'Unable to restore draft from server.'; const errorMsg = err?.error?.detail || err?.error?.message || 'Unable to restore organization details from server.';
this.toastr.error(errorMsg, 'Draft Restore Failed'); this.toastr.error(errorMsg, 'Organization Restoration Failed');
return of(null); return of(null);
}), }),
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
) )
.subscribe(draft => { .subscribe(draft => {
if (draft) { if (draft) {
this.stateService.restoreServerDraft(draft); this.stateService.restoreServerDraft(draft, true);
this.savedDraftSnapshot = this.serializeDraftState(); this.savedDraftSnapshot = this.serializeDraftState();
this.hydrateActiveStep(); this.hydrateActiveStep();
} }
@@ -197,7 +198,12 @@ export class OrganizationOnboarding {
const data = this.stateService.onboardingData(); const data = this.stateService.onboardingData();
if (!data.basics || !data.localization || !data.planLimits || !data.admin) { 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; 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 { onCancel(): void {
this.captureActiveStepDraft(); this.captureActiveStepDraft();
if (this.serializeDraftState() !== this.savedDraftSnapshot) { if (this.serializeDraftState() !== this.savedDraftSnapshot) {
@@ -399,7 +486,6 @@ export class OrganizationOnboarding {
private captureActiveStepDraft(): void { private captureActiveStepDraft(): void {
const currentIndex = this.currentStepIndex(); const currentIndex = this.currentStepIndex();
const validatedValue = this.getValidatedStepValue(currentIndex);
switch (currentIndex) { switch (currentIndex) {
case 0: case 0:
@@ -415,14 +501,6 @@ export class OrganizationOnboarding {
this.stateService.updateAdminDraft(this.adminStep()?.getDraftValue() ?? null); this.stateService.updateAdminDraft(this.adminStep()?.getDraftValue() ?? null);
break; 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 { private getValidatedStepValue(index: number): unknown {
@@ -109,47 +109,37 @@
<div> <div>
<dt class="text-xs text-textmuted">Organization</dt> <dt class="text-xs text-textmuted">Organization</dt>
<dd class="font-medium text-defaulttextcolor dark:text-white"> <dd class="font-medium text-defaulttextcolor dark:text-white">
{{ reviewData().basics?.organizationName || 'Not completed' }} {{ organizationNameSummary() }}
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-xs text-textmuted">Country</dt> <dt class="text-xs text-textmuted">Country</dt>
<dd class="font-medium text-defaulttextcolor dark:text-white"> <dd class="font-medium text-defaulttextcolor dark:text-white">
{{ reviewData().basics?.registrationCountry?.label || 'Not completed' }} {{ countrySummary() }}
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-xs text-textmuted">Localization</dt> <dt class="text-xs text-textmuted">Localization</dt>
<dd class="font-medium text-defaulttextcolor dark:text-white"> <dd class="font-medium text-defaulttextcolor dark:text-white">
{{ reviewData().localization?.timeZone?.label || 'Not completed' }} · {{ localizationSummary() }}
{{ reviewData().localization?.currency?.secondaryLabel || reviewData().localization?.currency?.label || '—' }}
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-xs text-textmuted">Plan and limits</dt> <dt class="text-xs text-textmuted">Plan and limits</dt>
<dd class="font-medium text-defaulttextcolor dark:text-white"> <dd class="font-medium text-defaulttextcolor dark:text-white">
{{ reviewData().planLimits?.subscriptionPlan?.label || 'Not completed' }} {{ planSummary() }}
@if (reviewData().planLimits) {
· {{ reviewData().planLimits?.maximumCompanies }} companies ·
{{ reviewData().planLimits?.maximumUsers }} users ·
{{ reviewData().planLimits?.maximumStorageGb }} GB
}
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-xs text-textmuted">Access dates</dt> <dt class="text-xs text-textmuted">Access dates</dt>
<dd class="font-medium text-defaulttextcolor dark:text-white"> <dd class="font-medium text-defaulttextcolor dark:text-white">
{{ reviewData().planLimits?.systemAccessStartDate || 'Not completed' }} {{ accessDatesSummary() }}
{{ reviewData().planLimits?.systemAccessEndDate || 'No end date' }}
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-xs text-textmuted">Administrator</dt> <dt class="text-xs text-textmuted">Administrator</dt>
<dd class="font-medium text-defaulttextcolor dark:text-white"> <dd class="font-medium text-defaulttextcolor dark:text-white">
{{ form.controls.administratorFullName.value || 'Not entered' }} {{ adminSummary() }}
@if (form.controls.administratorEmail.value) {
· {{ form.controls.administratorEmail.value }}
}
</dd> </dd>
</div> </div>
</dl> </dl>
@@ -2,10 +2,12 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
ElementRef, ElementRef,
computed,
inject, inject,
input, input,
signal, signal,
} from '@angular/core'; } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { FormInput } from '../../../../../shared/components/form/form-input/form-input'; import { FormInput } from '../../../../../shared/components/form/form-input/form-input';
@@ -45,6 +47,73 @@ export class OrganizationAdminStepComponent implements OnboardingStepForm<Organi
administratorMobile: ['', [Validators.required]], 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 { validate(): boolean {
this.submitAttempted.set(true); this.submitAttempted.set(true);
if (this.form.valid) { if (this.form.valid) {
@@ -71,12 +140,28 @@ export class OrganizationAdminStepComponent implements OnboardingStepForm<Organi
} }
patchValue(value: Partial<OrganizationAdminValue> & Record<string, any>): void { patchValue(value: Partial<OrganizationAdminValue> & Record<string, any>): void {
if (!value) return;
this.form.patchValue({ this.form.patchValue({
organizationEmail: value.organizationEmail ?? value['OrgEmail'] ?? value['orgEmail'] ?? '', organizationEmail: value.organizationEmail ?? value['OrgEmail'] ?? value['orgEmail'] ?? value['OrganizationEmail'] ?? '',
organizationPhone: value.organizationPhone ?? value['OrgPhone'] ?? value['orgPhone'] ?? '', organizationPhone: value.organizationPhone ?? value['OrgPhone'] ?? value['orgPhone'] ?? value['OrganizationPhone'] ?? '',
administratorFullName: value.administratorFullName ?? value['AdminFullName'] ?? value['adminFullName'] ?? '', administratorFullName: value.administratorFullName ?? value['AdminFullName'] ?? value['adminFullName'] ?? value['AdministratorFullName'] ?? '',
administratorEmail: value.administratorEmail ?? value['AdminEmail'] ?? value['adminEmail'] ?? '', administratorEmail: value.administratorEmail ?? value['AdminEmail'] ?? value['adminEmail'] ?? value['AdministratorEmail'] ?? '',
administratorMobile: value.administratorMobile ?? value['AdminMobile'] ?? value['adminMobile'] ?? '', 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 }); }, { emitEvent: false });
this.submitAttempted.set(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 { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { catchError, map, of } from 'rxjs'; import { catchError, map, of } from 'rxjs';
@@ -24,8 +25,10 @@ import {
OrganizationBasicsValue, OrganizationBasicsValue,
} from '../../models/organization-onboarding.model'; } from '../../models/organization-onboarding.model';
import { OrganizationOnboardingService } from '../../data-access/services/organization-onboarding.service'; import { OrganizationOnboardingService } from '../../data-access/services/organization-onboarding.service';
import { OrganizationOnboardingStateService } from '../../data-access/services/organization-onboarding-state.service';
interface OrganizationBasicsFormModel { interface OrganizationBasicsFormModel {
readonly code: string | null;
readonly organizationName: string; readonly organizationName: string;
readonly shortName: string; readonly shortName: string;
readonly localLanguageName: string; readonly localLanguageName: string;
@@ -42,6 +45,7 @@ interface OrganizationBasicsFormModel {
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class OrganizationBasicsStepComponent implements OnboardingStepForm<OrganizationBasicsValue> { export class OrganizationBasicsStepComponent implements OnboardingStepForm<OrganizationBasicsValue> {
private readonly destroyRef = inject(DestroyRef);
private readonly formBuilder = inject(FormBuilder); private readonly formBuilder = inject(FormBuilder);
private readonly onboardingService = inject(OrganizationOnboardingService); private readonly onboardingService = inject(OrganizationOnboardingService);
private readonly countryService = inject(CountryService); private readonly countryService = inject(CountryService);
@@ -49,6 +53,8 @@ export class OrganizationBasicsStepComponent implements OnboardingStepForm<Organ
private readonly toastr = inject(ToastrService); private readonly toastr = inject(ToastrService);
private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef); private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
private readonly stateService = inject(OrganizationOnboardingStateService, { optional: true });
readonly submitAttempted = signal(false); readonly submitAttempted = signal(false);
readonly organizationCodeControl = this.formBuilder.nonNullable.control('Pending generation'); readonly organizationCodeControl = this.formBuilder.nonNullable.control('Pending generation');
readonly organizationTypeSelection = signal<OnboardingLookupValue | null>(null); readonly organizationTypeSelection = signal<OnboardingLookupValue | null>(null);
@@ -152,8 +158,11 @@ export class OrganizationBasicsStepComponent implements OnboardingStepForm<Organ
throw new Error('Organization basics selections are incomplete.'); throw new Error('Organization basics selections are incomplete.');
} }
const codeControlVal = this.organizationCodeControl.value;
const codeVal = codeControlVal && codeControlVal !== 'Pending generation' ? codeControlVal : null;
return { return {
organizationCode: null, organizationCode: codeVal,
organizationName: value.organizationName.trim(), organizationName: value.organizationName.trim(),
shortName: value.shortName.trim(), shortName: value.shortName.trim(),
localLanguageName: value.localLanguageName.trim() || null, localLanguageName: value.localLanguageName.trim() || null,
@@ -169,45 +178,126 @@ export class OrganizationBasicsStepComponent implements OnboardingStepForm<Organ
getDraftValue(): Partial<OrganizationBasicsValue> { getDraftValue(): Partial<OrganizationBasicsValue> {
const value = this.form.getRawValue() as OrganizationBasicsFormModel; 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 { return {
organizationCode: null, organizationCode: codeVal,
organizationName: value.organizationName, organizationName: value.organizationName,
shortName: value.shortName, shortName: value.shortName,
localLanguageName: value.localLanguageName || null, localLanguageName: value.localLanguageName || null,
organizationType: this.organizationTypeSelection() ?? undefined, organizationType: this.organizationTypeSelection() ?? undefined,
industry: this.industrySelection() ?? undefined, industry: this.industrySelection() ?? undefined,
registrationCountry: this.registrationCountrySelection() registrationCountry: country
? { ? {
id: this.registrationCountrySelection()!.id, id: country.id,
label: this.registrationCountrySelection()!.name, label: country.name,
iso2: this.registrationCountrySelection()!.iso2, name: country.name,
} iso2: country.iso2,
} as any
: undefined, : 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({ this.form.patchValue({
organizationName: value.organizationName ?? '', organizationName: orgName,
shortName: value.shortName ?? '', shortName: shortName,
localLanguageName: value.localLanguageName ?? '', localLanguageName: localLanguageName,
organizationTypeId: value.organizationType?.id ?? null, organizationTypeId: orgTypeParsedId,
industryId: value.industry?.id ?? null, industryId: industryId,
registrationCountryId: value.registrationCountry?.id ?? null, registrationCountryId: countryId,
}, { emitEvent: false }); }, { emitEvent: false });
this.organizationTypeSelection.set(value.organizationType ?? null); // Handle Organization Type Selection
this.industrySelection.set(value.industry ?? null); if (orgTypeParsedId != null) {
this.registrationCountrySelection.set( if (orgTypeLabel) {
value.registrationCountry this.organizationTypeSelection.set({ id: orgTypeParsedId, label: orgTypeLabel });
? { } else {
id: value.registrationCountry.id, this.onboardingService.resolveOrganizationType(orgTypeParsedId).pipe(
iso2: value.registrationCountry.iso2 ?? '', catchError(() => of(null)),
name: value.registrationCountry.label, takeUntilDestroyed(this.destroyRef)
).subscribe(resolved => {
if (resolved) this.organizationTypeSelection.set(resolved);
});
} }
: null } 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); this.submitAttempted.set(false);
} }
@@ -289,12 +289,20 @@ export class OrganizationLocalizationStepComponent implements OnboardingStepForm
} }
patchValue(value: Partial<OrganizationLocalizationValue> & Record<string, any>): void { 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 rawTf = value.timeFormat !== undefined && value.timeFormat !== null ? value.timeFormat : value['TimeFormat'] ?? value['timeFormat'];
const timeFormatNormalized: TimeFormatValue | null = const timeFormatNormalized: TimeFormatValue | null =
rawTf === 1 || rawTf === '1' || rawTf === 'TwentyFourHour' rawTf === 1 || rawTf === '1' || rawTf === 'TwentyFourHour'
? 1 ? 'TwentyFourHour'
: rawTf === 0 || rawTf === '0' || rawTf === 'TwelveHour' : rawTf === 0 || rawTf === '0' || rawTf === 'TwelveHour'
? 0 ? 'TwelveHour'
: null; : null;
const rawFiscalYear = value.fiscalYearConvention ?? value['FiscalYearStart'] ?? value['fiscalYearStart'] ?? null; const rawFiscalYear = value.fiscalYearConvention ?? value['FiscalYearStart'] ?? value['fiscalYearStart'] ?? null;
@@ -304,46 +312,88 @@ export class OrganizationLocalizationStepComponent implements OnboardingStepForm
const numberFormatNormalized = mapApiToNumberFormat(rawNumberFormat); const numberFormatNormalized = mapApiToNumberFormat(rawNumberFormat);
this.form.patchValue({ this.form.patchValue({
timeZoneId: value.timeZone?.id ?? value['DefaultTimezoneId'] ?? value['defaultTimezoneId'] ?? value['timeZoneId'] ?? null, timeZoneId,
currencyId: value.currency?.id ?? value['DefaultCurrencyId'] ?? value['defaultCurrencyId'] ?? value['currencyId'] ?? null, currencyId,
defaultLanguageId: value.defaultLanguage?.id ?? value['DefaultLanguageId'] ?? value['defaultLanguageId'] ?? value['defaultLanguageId'] ?? null, defaultLanguageId,
additionalLanguageIds: value.additionalLanguageIds ?? value['AdditionalLanguageIds'] ?? value['additionalLanguageIds'] ?? [], additionalLanguageIds,
dateFormat: value.dateFormat ?? value['DateFormat'] ?? value['dateFormat'] ?? null, dateFormat,
timeFormat: timeFormatNormalized, timeFormat: timeFormatNormalized,
numberFormat: numberFormatNormalized, numberFormat: numberFormatNormalized,
fiscalYearConvention: fiscalYearNormalized, fiscalYearConvention: fiscalYearNormalized,
}, { emitEvent: false }); }, { emitEvent: false });
this.timeZoneSelection.set( // Timezone Selection
value.timeZone if (value.timeZone) {
? { this.timeZoneSelection.set({
id: value.timeZone.id, id: value.timeZone.id,
ianaId: value.timeZone.secondaryLabel ?? value.timeZone.label, ianaId: value.timeZone.secondaryLabel ?? value.timeZone.label,
displayName: 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 });
});
} }
: null } else {
); this.timeZoneSelection.set(null);
this.currencySelection.set( }
value.currency
? { // Currency Selection
if (value.currency) {
this.currencySelection.set({
id: value.currency.id, id: value.currency.id,
code: value.currency.secondaryLabel ?? value.currency.label, code: value.currency.secondaryLabel ?? value.currency.label,
name: value.currency.label, name: value.currency.label,
symbol: '', 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 });
});
} }
: null } else {
); this.currencySelection.set(null);
this.defaultLanguageSelection.set( }
value.defaultLanguage
? { // Default Language Selection
if (value.defaultLanguage) {
this.defaultLanguageSelection.set({
id: value.defaultLanguage.id, id: value.defaultLanguage.id,
code: value.defaultLanguage.secondaryLabel ?? value.defaultLanguage.label, code: value.defaultLanguage.secondaryLabel ?? value.defaultLanguage.label,
name: value.defaultLanguage.label, name: value.defaultLanguage.label,
nativeName: value.defaultLanguage.label, nativeName: value.defaultLanguage.label,
isRightToLeft: false, 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 });
});
} }
: null } else {
); this.defaultLanguageSelection.set(null);
}
this.additionalLanguageOptions.update(current => this.mergeSelectionOptions(current, value.additionalLanguageSelections ?? [])); this.additionalLanguageOptions.update(current => this.mergeSelectionOptions(current, value.additionalLanguageSelections ?? []));
this.submitAttempted.set(false); this.submitAttempted.set(false);
} }
@@ -224,32 +224,47 @@ export class OrganizationPlanLimitsStepComponent implements OnboardingStepForm<O
} }
patchValue(value: Partial<OrganizationPlanLimitsValue> & Record<string, any>): void { 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 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'] ? { const subPlan = value.subscriptionPlan ?? (value['SubscriptionPlan'] || value['plan'] || value['Plan'] ? {
id: value['SubscriptionPlan']?.id ?? planId, id: value['SubscriptionPlan']?.id ?? value['plan']?.id ?? value['Plan']?.id ?? planId,
label: value['SubscriptionPlan']?.name ?? value['SubscriptionPlan']?.label ?? '', label: value['SubscriptionPlan']?.name ?? value['SubscriptionPlan']?.label ?? value['planName'] ?? value['PlanName'] ?? value['plan']?.name ?? '',
code: value['SubscriptionPlan']?.code ?? '', code: value['SubscriptionPlan']?.code ?? value['plan']?.code ?? value['Plan']?.code ?? '',
} : planId ? { id: planId, label: '', code: '' } : null); } : planId ? { id: planId, label: value['PlanName'] ?? value['planName'] ?? '', code: '' } : null);
const from = value.systemAccessStartDate ?? value['SystemAccessFrom'] ?? null; const from = value.systemAccessStartDate ?? value['SystemAccessFrom'] ?? value['systemAccessFrom'] ?? value['systemAccessStartDate'] ?? null;
const to = value.systemAccessEndDate ?? value['SystemAccessTo'] ?? null; const to = value.systemAccessEndDate ?? value['SystemAccessTo'] ?? value['systemAccessTo'] ?? value['systemAccessEndDate'] ?? null;
const systemAccessPeriod: DateRangeValue | null = (from || to) ? { from, to } : null; const systemAccessPeriod: DateRangeValue | null = (from || to) ? { from, to } : null;
this.form.patchValue({ this.form.patchValue({
subscriptionPlanId: planId, subscriptionPlanId: planId,
licenseType: normalizedLicenseType, licenseType: normalizedLicenseType,
maximumCompanies: value.maximumCompanies ?? value['MaxCompanies'] ?? null, maximumCompanies: value.maximumCompanies ?? value['MaxCompanies'] ?? value['maxCompanies'] ?? null,
maximumUsers: value.maximumUsers ?? value['MaxUsers'] ?? null, maximumUsers: value.maximumUsers ?? value['MaxUsers'] ?? value['maxUsers'] ?? null,
maximumStorageGb: value.maximumStorageGb ?? value['MaxStorageGb'] ?? null, maximumStorageGb: value.maximumStorageGb ?? value['MaxStorageGb'] ?? value['maxStorageGb'] ?? null,
goLiveDate: value.goLiveDate ?? value['GoLiveDate'] ?? null, goLiveDate: value.goLiveDate ?? value['GoLiveDate'] ?? value['goLiveDate'] ?? null,
systemAccessPeriod, systemAccessPeriod,
}, { emitEvent: false }); }, { 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.limitsEditable.set(value.limitsEditable ?? true);
this.applyLimitsEditableState(); this.applyLimitsEditableState();
this.submitAttempted.set(false); this.submitAttempted.set(false);
@@ -265,10 +265,6 @@ export class TenantCurrencies {
return; return;
} }
if (response.draw !== query.draw) {
return;
}
const rows: TenantCurrencyTableRow[] = const rows: TenantCurrencyTableRow[] =
response.rows.map( response.rows.map(
(tenantCurrency, index) => ({ (tenantCurrency, index) => ({
@@ -2,7 +2,7 @@
[columns]="columns()" [columns]="columns()"
[rows]="tableStore.rows()" [rows]="tableStore.rows()"
[actions]="actions()" [actions]="actions()"
[totalRecords]="tableStore.totalRecords()" [totalRecords]="tableStore.filteredRecords()"
[pageIndex]="tableStore.queryState.pageIndex()" [pageIndex]="tableStore.queryState.pageIndex()"
[pageSize]="tableStore.queryState.pageSize()" [pageSize]="tableStore.queryState.pageSize()"
tableTitle="Tenants" tableTitle="Tenants"
+3 -1
View File
@@ -84,7 +84,9 @@
solid button ('Next') to outline style and remove its glow/box-shadow. 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(.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; background: #ffffff !important;
color: var(--primary, #7c3deb) !important; color: var(--primary, #7c3deb) !important;
border: 1.5px solid var(--primary, #7c3deb) !important; border: 1.5px solid var(--primary, #7c3deb) !important;
@@ -114,7 +114,7 @@
</tr> </tr>
} @else } @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)"> <tr [class]="getRowClass(row, $index)" (click)="onRowClick(row)">
@for (column of columns(); track column.key) { @for (column of columns(); track column.key) {
<td [class]="getCellClass(column)"> <td [class]="getCellClass(column)">
@@ -58,9 +58,8 @@ export class DataTableStore<TItem, TRow extends DataTableRecord = TItem & DataTa
if (!response) return; if (!response) return;
const currentQuery = this.queryState.getQuery(); const currentQuery = this.queryState.getQuery();
if (response.draw !== currentQuery.draw) return; const rowsList = response.rows || [];
const mappedRows: TRow[] = rowsList.map((item, index) => {
const mappedRows: TRow[] = response.rows.map((item, index) => {
const serialNumber = (currentQuery.page - 1) * currentQuery.pageSize + index + 1; const serialNumber = (currentQuery.page - 1) * currentQuery.pageSize + index + 1;
if (options.mapRow) { if (options.mapRow) {
return options.mapRow(item, serialNumber, currentQuery); return options.mapRow(item, serialNumber, currentQuery);
@@ -72,8 +71,8 @@ export class DataTableStore<TItem, TRow extends DataTableRecord = TItem & DataTa
}); });
this.rows.set(mappedRows); this.rows.set(mappedRows);
this.totalRecords.set(response.total); this.totalRecords.set(response.total ?? 0);
this.filteredRecords.set(response.filtered); this.filteredRecords.set(response.filtered ?? response.total ?? 0);
}); });
this.load(this.queryState.getQuery()); this.load(this.queryState.getQuery());