From 368d6bd5405aff509e6833610a9b2c0ebad2a804 Mon Sep 17 00:00:00 2001 From: Gagan7900 <54118395+Gagan7900@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:30:51 +0530 Subject: [PATCH] organization onboarding list, edit case changes --- .../organizations/dashboard/dashboard.html | 2 +- .../organizations/dashboard/dashboard.ts | 193 ++++------------- .../data-access/organization.endpoints.ts | 20 ++ .../data-access/organization.service.ts | 41 ++++ .../models/organization.model.ts | 67 ++++++ .../organization-list/organization-list.html | 2 +- .../organization-list/organization-list.ts | 197 ++++-------------- .../onboarding-stepper.html | 20 ++ .../onboarding-stepper/onboarding-stepper.ts | 11 + .../organization-onboarding-state.service.ts | 75 +++++-- .../models/organization-onboarding.model.ts | 8 + .../organization-onboarding.html | 2 + .../organization-onboarding.ts | 106 ++++++++-- .../organization-admin.html | 22 +- .../organization-admin/organization-admin.ts | 95 ++++++++- .../organization-basics.ts | 142 ++++++++++--- .../organization-localization.ts | 124 +++++++---- .../organization-plan-limits.ts | 43 ++-- .../tenant-currencies/tenant-currencies.ts | 4 - .../pages/tenant-list/tenant-list.html | 2 +- src/app/shared/components/button/button.scss | 4 +- .../components/data-table/data-table.html | 2 +- .../components/data-table/data-table.store.ts | 9 +- 23 files changed, 746 insertions(+), 445 deletions(-) create mode 100644 src/app/features/organizations/organization-list/data-access/organization.endpoints.ts create mode 100644 src/app/features/organizations/organization-list/data-access/organization.service.ts create mode 100644 src/app/features/organizations/organization-list/models/organization.model.ts diff --git a/src/app/features/organizations/dashboard/dashboard.html b/src/app/features/organizations/dashboard/dashboard.html index 692de853..55d14354 100644 --- a/src/app/features/organizations/dashboard/dashboard.html +++ b/src/app/features/organizations/dashboard/dashboard.html @@ -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" diff --git a/src/app/features/organizations/dashboard/dashboard.ts b/src/app/features/organizations/dashboard/dashboard.ts index 2e40a8a0..91718d18 100644 --- a/src/app/features/organizations/dashboard/dashboard.ts +++ b/src/app/features/organizations/dashboard/dashboard.ts @@ -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); + private readonly organizationApi = inject(OrganizationService); + readonly tableStore = inject(DataTableStore); readonly statCards = signal([ { @@ -107,100 +93,8 @@ export class Dashboard implements OnInit { }, ]); - readonly recentOrganizations = signal([ - { - 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[]>([ + readonly columns = signal[]>([ + { 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('No Organizations'); readonly emptyDescription = signal('Start by adding your first organization'); - readonly actions = signal[]>([ + readonly actions = signal[]>([ { 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 = { - 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): void { + onActionClick(event: DataTableActionEvent): 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 { } } + diff --git a/src/app/features/organizations/organization-list/data-access/organization.endpoints.ts b/src/app/features/organizations/organization-list/data-access/organization.endpoints.ts new file mode 100644 index 00000000..f00232c6 --- /dev/null +++ b/src/app/features/organizations/organization-list/data-access/organization.endpoints.ts @@ -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; + diff --git a/src/app/features/organizations/organization-list/data-access/organization.service.ts b/src/app/features/organizations/organization-list/data-access/organization.service.ts new file mode 100644 index 00000000..f58fab92 --- /dev/null +++ b/src/app/features/organizations/organization-list/data-access/organization.service.ts @@ -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> { + const payload = { + ...query, + ...(countryId ? { countryId } : {}) + }; + return this.http.post>(ORGANIZATION_ENDPOINTS.dataTable, payload); + } + + getOrganizationById(id: string): Observable { + return this.http.get(ORGANIZATION_ENDPOINTS.getById(id)); + } + + autocomplete(term?: string, limit = 10): Observable { + let params = new HttpParams().set('limit', limit); + const normalizedTerm = term?.trim(); + + if (normalizedTerm) { + params = params.set('term', normalizedTerm); + } + + return this.http.get(ORGANIZATION_ENDPOINTS.autocomplete, { params }); + } +} diff --git a/src/app/features/organizations/organization-list/models/organization.model.ts b/src/app/features/organizations/organization-list/models/organization.model.ts new file mode 100644 index 00000000..aef497d3 --- /dev/null +++ b/src/app/features/organizations/organization-list/models/organization.model.ts @@ -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'; + diff --git a/src/app/features/organizations/organization-list/organization-list.html b/src/app/features/organizations/organization-list/organization-list.html index 2fe0c7fa..ead650b7 100644 --- a/src/app/features/organizations/organization-list/organization-list.html +++ b/src/app/features/organizations/organization-list/organization-list.html @@ -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" diff --git a/src/app/features/organizations/organization-list/organization-list.ts b/src/app/features/organizations/organization-list/organization-list.ts index 7be4918a..4622ff9a 100644 --- a/src/app/features/organizations/organization-list/organization-list.ts +++ b/src/app/features/organizations/organization-list/organization-list.ts @@ -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); + private readonly organizationApi = inject(OrganizationService); + readonly tableStore = inject(DataTableStore); readonly selectedCountryLookup = signal(null); readonly appliedCountryId = signal(null); @@ -59,104 +45,11 @@ export class OrganizationList implements OnInit { countryId: [''], }); - readonly allOrganizations = signal([ - { - 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 = (term, limit) => this.countryApi.autocomplete(term, limit); readonly displayCountry: AutocompleteDisplayFn = country => country.name; readonly countryValue: AutocompleteValueFn = country => country.id; - readonly columns = signal[]>([ + readonly columns = signal[]>([ { 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[]>([ + readonly actions = signal[]>([ { 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 = { - 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): void { + onActionClick(event: DataTableActionEvent): 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 { } } + diff --git a/src/app/features/organizations/organization-onboarding/components/onboarding-stepper/onboarding-stepper.html b/src/app/features/organizations/organization-onboarding/components/onboarding-stepper/onboarding-stepper.html index 30391530..cd0bce1b 100644 --- a/src/app/features/organizations/organization-onboarding/components/onboarding-stepper/onboarding-stepper.html +++ b/src/app/features/organizations/organization-onboarding/components/onboarding-stepper/onboarding-stepper.html @@ -110,6 +110,25 @@
+ @if (isEditMode()) { +
+ +
+ +
+ + +
+ +
+
+ } @else {
@@ -153,6 +172,7 @@ }
+ }
diff --git a/src/app/features/organizations/organization-onboarding/components/onboarding-stepper/onboarding-stepper.ts b/src/app/features/organizations/organization-onboarding/components/onboarding-stepper/onboarding-stepper.ts index 6f2cd35b..ae3da4b3 100644 --- a/src/app/features/organizations/organization-onboarding/components/onboarding-stepper/onboarding-stepper.ts +++ b/src/app/features/organizations/organization-onboarding/components/onboarding-stepper/onboarding-stepper.ts @@ -24,6 +24,8 @@ export class OnboardingStepper { readonly currentStepIndex = input.required(); readonly completedStepIndexes = input([]); + 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(); readonly nextClicked = output(); readonly saveDraftClicked = output(); + readonly saveChangesClicked = output(); readonly finishClicked = output(); readonly cancelClicked = output(); @@ -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() || diff --git a/src/app/features/organizations/organization-onboarding/data-access/services/organization-onboarding-state.service.ts b/src/app/features/organizations/organization-onboarding/data-access/services/organization-onboarding-state.service.ts index dea1558a..afea86bc 100644 --- a/src/app/features/organizations/organization-onboarding/data-access/services/organization-onboarding-state.service.ts +++ b/src/app/features/organizations/organization-onboarding/data-access/services/organization-onboarding-state.service.ts @@ -35,6 +35,7 @@ const INITIAL_DRAFT_STATE: OnboardingDraftState = { @Injectable() export class OrganizationOnboardingStateService { + private readonly isEditModeState = signal(false); private readonly organizationIdState = signal(null); private readonly onboardingDataState = signal(INITIAL_DATA); private readonly onboardingDraftState = signal(INITIAL_DRAFT_STATE); @@ -43,6 +44,7 @@ export class OrganizationOnboardingStateService { private readonly savedDraftIdState = signal(null); private readonly savedAtState = signal(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; + 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); diff --git a/src/app/features/organizations/organization-onboarding/models/organization-onboarding.model.ts b/src/app/features/organizations/organization-onboarding/models/organization-onboarding.model.ts index 9fb16e4e..cadf8686 100644 --- a/src/app/features/organizations/organization-onboarding/models/organization-onboarding.model.ts +++ b/src/app/features/organizations/organization-onboarding/models/organization-onboarding.model.ts @@ -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, 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, diff --git a/src/app/features/organizations/organization-onboarding/organization-onboarding.html b/src/app/features/organizations/organization-onboarding/organization-onboarding.html index 563d4284..701ff693 100644 --- a/src/app/features/organizations/organization-onboarding/organization-onboarding.html +++ b/src/app/features/organizations/organization-onboarding/organization-onboarding.html @@ -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()" > diff --git a/src/app/features/organizations/organization-onboarding/organization-onboarding.ts b/src/app/features/organizations/organization-onboarding/organization-onboarding.ts index fbf259f9..f34f3edf 100644 --- a/src/app/features/organizations/organization-onboarding/organization-onboarding.ts +++ b/src/app/features/organizations/organization-onboarding/organization-onboarding.ts @@ -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; + + 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 { diff --git a/src/app/features/organizations/organization-onboarding/steps/organization-admin/organization-admin.html b/src/app/features/organizations/organization-onboarding/steps/organization-admin/organization-admin.html index 9f0ca37b..3e233f97 100644 --- a/src/app/features/organizations/organization-onboarding/steps/organization-admin/organization-admin.html +++ b/src/app/features/organizations/organization-onboarding/steps/organization-admin/organization-admin.html @@ -109,47 +109,37 @@
Organization
- {{ reviewData().basics?.organizationName || 'Not completed' }} + {{ organizationNameSummary() }}
Country
- {{ reviewData().basics?.registrationCountry?.label || 'Not completed' }} + {{ countrySummary() }}
Localization
- {{ reviewData().localization?.timeZone?.label || 'Not completed' }} · - {{ reviewData().localization?.currency?.secondaryLabel || reviewData().localization?.currency?.label || '—' }} + {{ localizationSummary() }}
Plan and limits
- {{ reviewData().planLimits?.subscriptionPlan?.label || 'Not completed' }} - @if (reviewData().planLimits) { - · {{ reviewData().planLimits?.maximumCompanies }} companies · - {{ reviewData().planLimits?.maximumUsers }} users · - {{ reviewData().planLimits?.maximumStorageGb }} GB - } + {{ planSummary() }}
Access dates
- {{ reviewData().planLimits?.systemAccessStartDate || 'Not completed' }} – - {{ reviewData().planLimits?.systemAccessEndDate || 'No end date' }} + {{ accessDatesSummary() }}
Administrator
- {{ form.controls.administratorFullName.value || 'Not entered' }} - @if (form.controls.administratorEmail.value) { - · {{ form.controls.administratorEmail.value }} - } + {{ adminSummary() }}
diff --git a/src/app/features/organizations/organization-onboarding/steps/organization-admin/organization-admin.ts b/src/app/features/organizations/organization-onboarding/steps/organization-admin/organization-admin.ts index 941a2f50..3c11ce32 100644 --- a/src/app/features/organizations/organization-onboarding/steps/organization-admin/organization-admin.ts +++ b/src/app/features/organizations/organization-onboarding/steps/organization-admin/organization-admin.ts @@ -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 { + const basics = this.reviewData()?.basics as Record | null; + return ( + basics?.['organizationName'] || + basics?.['name'] || + basics?.['OrganizationName'] || + basics?.['Name'] || + 'Not completed' + ); + }); + + readonly countrySummary = computed(() => { + const basics = this.reviewData()?.basics as Record | 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 | 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 | 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 | 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 | 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 & Record): 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); } diff --git a/src/app/features/organizations/organization-onboarding/steps/organization-basics/organization-basics.ts b/src/app/features/organizations/organization-onboarding/steps/organization-basics/organization-basics.ts index cfd246c0..74f5c80b 100644 --- a/src/app/features/organizations/organization-onboarding/steps/organization-basics/organization-basics.ts +++ b/src/app/features/organizations/organization-onboarding/steps/organization-basics/organization-basics.ts @@ -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 { + 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>(ElementRef); + private readonly stateService = inject(OrganizationOnboardingStateService, { optional: true }); + readonly submitAttempted = signal(false); readonly organizationCodeControl = this.formBuilder.nonNullable.control('Pending generation'); readonly organizationTypeSelection = signal(null); @@ -152,8 +158,11 @@ export class OrganizationBasicsStepComponent implements OnboardingStepForm { 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): void { + patchValue(value: Partial & Record): void { + if (!value) return; + + const draftBasics = this.stateService?.onboardingDraft()?.basics as Record | 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); } diff --git a/src/app/features/organizations/organization-onboarding/steps/organization-localization/organization-localization.ts b/src/app/features/organizations/organization-onboarding/steps/organization-localization/organization-localization.ts index c39e58b9..721307ce 100644 --- a/src/app/features/organizations/organization-onboarding/steps/organization-localization/organization-localization.ts +++ b/src/app/features/organizations/organization-onboarding/steps/organization-localization/organization-localization.ts @@ -289,12 +289,20 @@ export class OrganizationLocalizationStepComponent implements OnboardingStepForm } patchValue(value: Partial & Record): 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); } diff --git a/src/app/features/organizations/organization-onboarding/steps/organization-plan-limits/organization-plan-limits.ts b/src/app/features/organizations/organization-onboarding/steps/organization-plan-limits/organization-plan-limits.ts index b4fa15be..05485576 100644 --- a/src/app/features/organizations/organization-onboarding/steps/organization-plan-limits/organization-plan-limits.ts +++ b/src/app/features/organizations/organization-onboarding/steps/organization-plan-limits/organization-plan-limits.ts @@ -224,32 +224,47 @@ export class OrganizationPlanLimitsStepComponent implements OnboardingStepForm & Record): 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); diff --git a/src/app/features/tenants/pages/tenant-currencies/tenant-currencies.ts b/src/app/features/tenants/pages/tenant-currencies/tenant-currencies.ts index 1205b463..b62721b0 100644 --- a/src/app/features/tenants/pages/tenant-currencies/tenant-currencies.ts +++ b/src/app/features/tenants/pages/tenant-currencies/tenant-currencies.ts @@ -265,10 +265,6 @@ export class TenantCurrencies { return; } - if (response.draw !== query.draw) { - return; - } - const rows: TenantCurrencyTableRow[] = response.rows.map( (tenantCurrency, index) => ({ diff --git a/src/app/features/tenants/pages/tenant-list/tenant-list.html b/src/app/features/tenants/pages/tenant-list/tenant-list.html index 4232f9d5..a64b2dec 100644 --- a/src/app/features/tenants/pages/tenant-list/tenant-list.html +++ b/src/app/features/tenants/pages/tenant-list/tenant-list.html @@ -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" diff --git a/src/app/shared/components/button/button.scss b/src/app/shared/components/button/button.scss index fdbe49b0..b023c563 100644 --- a/src/app/shared/components/button/button.scss +++ b/src/app/shared/components/button/button.scss @@ -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; diff --git a/src/app/shared/components/data-table/data-table.html b/src/app/shared/components/data-table/data-table.html index 638bb90f..96a64d6c 100644 --- a/src/app/shared/components/data-table/data-table.html +++ b/src/app/shared/components/data-table/data-table.html @@ -114,7 +114,7 @@ } @else { - @for (row of rows(); track row['id']; let rowIndex = $index) { + @for (row of rows(); track row['id'] ?? $index; let rowIndex = $index) { @for (column of columns(); track column.key) { diff --git a/src/app/shared/components/data-table/data-table.store.ts b/src/app/shared/components/data-table/data-table.store.ts index e881eec4..ce426230 100644 --- a/src/app/shared/components/data-table/data-table.store.ts +++ b/src/app/shared/components/data-table/data-table.store.ts @@ -58,9 +58,8 @@ export class DataTableStore { + 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