organization onboarding list, edit case changes
This commit is contained in:
@@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+20
@@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+20
@@ -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>
|
||||
|
||||
+11
@@ -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() ||
|
||||
|
||||
+61
-14
@@ -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);
|
||||
|
||||
+8
@@ -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 {
|
||||
|
||||
+6
-16
@@ -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>
|
||||
|
||||
+90
-5
@@ -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);
|
||||
}
|
||||
|
||||
+116
-26
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+87
-37
@@ -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);
|
||||
}
|
||||
|
||||
+29
-14
@@ -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"
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user