diff --git a/public/assets/fonts/EtihadAltis-Text_V3.ttf b/public/assets/fonts/EtihadAltis-Text_V3.ttf new file mode 100644 index 00000000..ec5eedad Binary files /dev/null and b/public/assets/fonts/EtihadAltis-Text_V3.ttf differ diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 45e88166..f3f36d81 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,5 +1,5 @@ import { ApplicationConfig, importProvidersFrom } from '@angular/core'; -import { provideRouter, withRouterConfig } from '@angular/router'; +import { provideRouter, withRouterConfig, RouteReuseStrategy } from '@angular/router'; import { provideAnimations } from '@angular/platform-browser/animations'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { App_Route } from './app.routes'; @@ -8,10 +8,12 @@ import { provideCharts, withDefaultRegisterables } from 'ng2-charts'; import { authInterceptor } from './core/interceptors/auth.interceptor'; import { errorInterceptor } from './core/interceptors/error.interceptor'; import { loadingInterceptor } from './core/interceptors/loading-interceptor'; +import { ReloadRouteReuseStrategy } from './core/strategies/reload-route-reuse.strategy'; export const appConfig: ApplicationConfig = { providers: [ provideRouter(App_Route, withRouterConfig({ onSameUrlNavigation: 'reload' })), + { provide: RouteReuseStrategy, useClass: ReloadRouteReuseStrategy }, provideHttpClient(withInterceptors([authInterceptor, errorInterceptor,loadingInterceptor])), provideAnimations(), provideCharts(withDefaultRegisterables()), diff --git a/src/app/core/services/common/menu.data.ts b/src/app/core/services/common/menu.data.ts index 4000dead..eb116e78 100644 --- a/src/app/core/services/common/menu.data.ts +++ b/src/app/core/services/common/menu.data.ts @@ -12,7 +12,7 @@ export const SAAS_MENU_DATA: MenuContext = { selected: false, dirchange: false, children: [ - { path: '/dashboards/crm', title: 'CRM', type: 'link', dirchange: false }, + { path: '/dashboards/crm', title: 'CRM', type: 'link', dirchange: false, icon: '' }, ], }, //{ headTitle: 'SAAS ADMIN' }, @@ -24,15 +24,15 @@ export const SAAS_MENU_DATA: MenuContext = { selected: false, dirchange: false, children: [ - { path: '/global-masters/currencies', title: 'Currency', type: 'link', dirchange: false }, - { path: '/global-masters/exchange-rates', title: 'Exchange Rates (ROE)', type: 'link', dirchange: false }, - { path: '/global-masters/languages', title: 'Language', type: 'link', dirchange: false }, - { path: '/global-masters/timezones', title: 'Timezone', type: 'link', dirchange: false }, - { path: '/global-masters/regions', title: 'Region', type: 'link', dirchange: false }, - { path: '/global-masters/countries', title: 'Country', type: 'link', dirchange: false }, - { path: '/global-masters/states', title: 'State', type: 'link', dirchange: false }, - { path: '/global-masters/cities', title: 'City', type: 'link', dirchange: false }, - { path: '/global-masters/plans', title: 'Plans', type: 'link', dirchange: false }, + { path: '/global-masters/currencies', title: 'Currency', type: 'link', dirchange: false, icon: '' }, + { path: '/global-masters/exchange-rates', title: 'Exchange Rates', type: 'link', dirchange: false, icon: '' }, + { path: '/global-masters/languages', title: 'Language', type: 'link', dirchange: false, icon: '' }, + { path: '/global-masters/timezones', title: 'Timezone', type: 'link', dirchange: false, icon: '' }, + { path: '/global-masters/regions', title: 'Region', type: 'link', dirchange: false, icon: '' }, + { path: '/global-masters/countries', title: 'Country', type: 'link', dirchange: false, icon: '' }, + { path: '/global-masters/states', title: 'State', type: 'link', dirchange: false, icon: '' }, + { path: '/global-masters/cities', title: 'City', type: 'link', dirchange: false, icon: '' }, + { path: '/global-masters/plans', title: 'Plans', type: 'link', dirchange: false, icon: '' }, // { // title: 'Tenant Master', @@ -55,10 +55,10 @@ export const SAAS_MENU_DATA: MenuContext = { selected: false, dirchange: false, children: [ - { path: '/organizations', title: 'Dashboard', type: 'link', dirchange: false }, - { path: '/organizations/list', title: 'Organization List', type: 'link', dirchange: false }, - { path: '/organizations/awaiting-database', title: 'Assign Database & Activate', type: 'link', dirchange: false }, - { path: '/organizations/tenant-domains', title: 'Tenant Domains', type: 'link', dirchange: false }, + { path: '/organizations', title: 'Dashboard', type: 'link', dirchange: false, icon: '' }, + { path: '/organizations/list', title: 'Organization List', type: 'link', dirchange: false, icon: '' }, + { path: '/organizations/awaiting-database', title: 'Assign Database', type: 'link', dirchange: false, icon: '' }, + { path: '/organizations/tenant-domains', title: 'Tenant Domains', type: 'link', dirchange: false, icon: '' }, ], }, { @@ -68,9 +68,9 @@ export const SAAS_MENU_DATA: MenuContext = { active: false, selected: false, dirchange: false, - children: [ - { path: '/localization/translation-keys', title: 'Translations Keys', type: 'link', dirchange: false }, - { path: '/localization/translations-manager', title: 'Translations Manager', type: 'link', dirchange: false }, + children: [ + { path: '/localization/translation-keys', title: 'Translations Keys', type: 'link', dirchange: false, icon: '' }, + { path: '/localization/translations-manager', title: 'Translations', type: 'link', dirchange: false, icon: '' }, ], }, { @@ -81,7 +81,7 @@ export const SAAS_MENU_DATA: MenuContext = { selected: false, dirchange: false, children: [ - { path: '/billing/plans-subscriptions', title: 'Plans & Subscriptions', type: 'link', dirchange: false }, + { path: '/billing/plans-subscriptions', title: 'Subscriptions', type: 'link', dirchange: false, icon: '' }, ], }, { @@ -92,8 +92,8 @@ export const SAAS_MENU_DATA: MenuContext = { selected: false, dirchange: false, children: [ - { path: '/settings/branding', title: 'Branding', type: 'link', dirchange: false }, - { path: '/settings/db-connections', title: 'Database Connections', type: 'link', dirchange: false }, + { path: '/settings/branding', title: 'Branding', type: 'link', dirchange: false, icon: '' }, + { path: '/settings/db-connections', title: 'Database Connections', type: 'link', dirchange: false, icon: '' }, ], }, ], diff --git a/src/app/core/strategies/reload-route-reuse.strategy.ts b/src/app/core/strategies/reload-route-reuse.strategy.ts new file mode 100644 index 00000000..d35e91cf --- /dev/null +++ b/src/app/core/strategies/reload-route-reuse.strategy.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@angular/core'; +import { BaseRouteReuseStrategy } from '@angular/router'; + +@Injectable() +export class ReloadRouteReuseStrategy extends BaseRouteReuseStrategy { + override shouldReuseRoute(): boolean { + return false; + } +} diff --git a/src/app/features/billing/plans-subscriptions/pages/plans-subscriptions/plans-subscriptions.html b/src/app/features/billing/plans-subscriptions/pages/plans-subscriptions/plans-subscriptions.html index 1564bc9e..a8154ea0 100644 --- a/src/app/features/billing/plans-subscriptions/pages/plans-subscriptions/plans-subscriptions.html +++ b/src/app/features/billing/plans-subscriptions/pages/plans-subscriptions/plans-subscriptions.html @@ -7,6 +7,8 @@ [totalRecords]="tableStore.totalRecords()" [pageIndex]="tableStore.queryState.pageIndex()" [pageSize]="tableStore.queryState.pageSize()" + [initialSortColumn]="tableStore.queryState.sortColumn()" + [initialSortDirection]="tableStore.queryState.sortDirection()" tableTitle="Tenant Subscriptions" buttonTitle="Add" [showSearch]="true" diff --git a/src/app/features/billing/plans-subscriptions/pages/plans-subscriptions/plans-subscriptions.ts b/src/app/features/billing/plans-subscriptions/pages/plans-subscriptions/plans-subscriptions.ts index 2f6461e8..321298fe 100644 --- a/src/app/features/billing/plans-subscriptions/pages/plans-subscriptions/plans-subscriptions.ts +++ b/src/app/features/billing/plans-subscriptions/pages/plans-subscriptions/plans-subscriptions.ts @@ -117,21 +117,21 @@ export class PlansSubscriptions implements OnInit { }); readonly columns = signal[]>([ - { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '90px' }, - { key: 'tenantName', label: 'Tenant / Organization', header: 'Tenant / Organization', sortable: true, align: 'left' }, + { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '6%' }, + { key: 'tenantName', label: 'Tenant / Organization', header: 'Tenant / Organization', sortable: true, align: 'left', width: '24%' , headerAlign: 'left'}, { - key: 'planName', label: 'Plan', header: 'Plan', sortable: true, align: 'left', + key: 'planName', label: 'Plan', header: 'Plan', sortable: true, align: 'left', headerAlign: 'left', width: '18%', formatter: (value, row) => `${row.planCode ? row.planCode + ' - ' : ''}${value ?? '—'}` }, { - key: 'status', label: 'Status', header: 'Status', sortable: true, badge: true, + key: 'status', label: 'Status', header: 'Status', sortable: true, badge: true, headerAlign: 'center', align: 'center', width: '12%', badgeClass: value => subscriptionStatusBadgeClass(value as SubscriptionStatus), formatter: value => SUBSCRIPTION_STATUS_LABELS[value as SubscriptionStatus] ?? '—' }, - { key: 'startsOn', label: 'Starts On', header: 'Starts On', sortable: true, align: 'left', formatter: value => formatDate(value as string) }, - { key: 'endsOn', label: 'Ends On', header: 'Ends On', sortable: true, align: 'left', formatter: value => formatDate(value as string | null) }, + { key: 'startsOn', label: 'Starts On', header: 'Starts On', sortable: true,headerAlign: 'center', align: 'center', width: '13%', formatter: value => formatDate(value as string) }, + { key: 'endsOn', label: 'Ends On', header: 'Ends On', sortable: true, headerAlign: 'center', align: 'center', width: '13%', formatter: value => formatDate(value as string | null) }, { - key: 'isActive', label: 'Active', header: 'Active', sortable: true, badge: true, + key: 'isActive', label: 'Active', header: 'Active', sortable: true, badge: true, headerAlign: 'center', align: 'center', width: '10%', badgeClass: value => value === true ? 'badge bg-success/10 text-success' : 'badge bg-danger/10 text-danger', formatter: value => value ? 'Yes' : 'No' } @@ -154,6 +154,7 @@ export class PlansSubscriptions implements OnInit { ngOnInit(): void { this.tableStore.initialize({ + defaultSort: { column: 'tenantName' }, fetcher: query => this.subscriptionApi.getSubscriptionDataTable(query), mapRow: (sub, serialNumber) => ({ id: sub.id, diff --git a/src/app/features/global-masters/cities/data-access/city.service.ts b/src/app/features/global-masters/cities/data-access/city.service.ts index fb6caba2..d2d00c1f 100644 --- a/src/app/features/global-masters/cities/data-access/city.service.ts +++ b/src/app/features/global-masters/cities/data-access/city.service.ts @@ -20,12 +20,16 @@ export class CityService { getCityDataTable( query: DataTableQuery, - countryId: string | null = null, - stateId: string | null = null + countryIds: readonly string[] = [], + stateIds: readonly string[] = [] ): Observable> { let params = new HttpParams(); - if (countryId) params = params.set('countryId', countryId); - if (stateId) params = params.set('stateId', stateId); + for (const countryId of countryIds) { + params = params.append('countryIds', countryId); + } + for (const stateId of stateIds) { + params = params.append('stateIds', stateId); + } return this.http.post>( CITY_ENDPOINTS.dataTable, query, diff --git a/src/app/features/global-masters/cities/pages/city-list/city-list.html b/src/app/features/global-masters/cities/pages/city-list/city-list.html index f08ad1b1..adedfb84 100644 --- a/src/app/features/global-masters/cities/pages/city-list/city-list.html +++ b/src/app/features/global-masters/cities/pages/city-list/city-list.html @@ -5,6 +5,8 @@ [totalRecords]="tableStore.totalRecords()" [pageIndex]="tableStore.queryState.pageIndex()" [pageSize]="tableStore.queryState.pageSize()" + [initialSortColumn]="tableStore.queryState.sortColumn()" + [initialSortDirection]="tableStore.queryState.sortDirection()" tableTitle="Cities" buttonTitle="Add" [showSearch]="true" @@ -24,46 +26,44 @@
-
-
diff --git a/src/app/features/global-masters/cities/pages/city-list/city-list.ts b/src/app/features/global-masters/cities/pages/city-list/city-list.ts index fbc242b1..74a83561 100644 --- a/src/app/features/global-masters/cities/pages/city-list/city-list.ts +++ b/src/app/features/global-masters/cities/pages/city-list/city-list.ts @@ -1,13 +1,11 @@ import { Component, DestroyRef, OnInit, inject, signal, viewChild } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; -import { of } from 'rxjs'; -import { catchError, finalize } from 'rxjs/operators'; +import { Subject, of } from 'rxjs'; +import { debounceTime, distinctUntilChanged, finalize, map, switchMap } from 'rxjs/operators'; import { NotificationService } from '../../../../../core/services/common/notification.service'; import { CityDto, UpdateCityRequest } from '../../models/city.model'; -import { CountryLookupDto } from '../../../countries/models/country.model'; -import { StateLookupDto } from '../../../states/models/state.model'; import { CityService } from '../../data-access/city.service'; import { CountryService } from '../../../countries/data-access/country.service'; import { StateService } from '../../../states/data-access/state.service'; @@ -19,12 +17,8 @@ import { DataTableColumn, DataTableRecord } from '../../../../../shared/components/data-table/data-table.types'; -import { Autocomplete } from '../../../../../shared/components/form/autocomplete/autocomplete'; -import { - AutocompleteDisplayFn, - AutocompleteSearchFn, - AutocompleteValueFn -} from '../../../../../shared/components/form/autocomplete/autocomplete.types'; +import { FormSelect } from '../../../../../shared/components/form/form-select/form-select'; +import { FormSelectOption } from '../../../../../shared/components/form/models/form-select.models'; import { Button } from '../../../../../shared/components/button/button'; import { ConfirmDialog } from '../../../../../shared/components/confirm-dialog/confirm-dialog'; import { CityFormModalComponent } from '../../components/city-form-modal/city-form-modal'; @@ -52,7 +46,7 @@ interface CityTableRow extends DataTableRecord { DataTable, DataTableToolbarDirective, ReactiveFormsModule, - Autocomplete, + FormSelect, Button, ConfirmDialog, CityFormModalComponent @@ -70,8 +64,6 @@ export class CityList implements OnInit { private readonly notification = inject(NotificationService); readonly tableStore = inject(DataTableStore); - readonly selectedCountry = signal(null); - readonly selectedFilterState = signal(null); readonly statusChangingId = signal(null); readonly deletingId = signal(null); readonly pendingDeleteCity = signal(null); @@ -80,18 +72,25 @@ export class CityList implements OnInit { readonly showFilters = signal(false); readonly filterForm = this.formBuilder.nonNullable.group({ - countryId: [''], - stateId: [{ value: '', disabled: true }] + countryId: [[] as string[]], + stateId: [{ value: [] as string[], disabled: true }] }); + readonly countryOptions = signal[]>([]); + readonly countryOptionsLoading = signal(false); + readonly stateOptions = signal[]>([]); + readonly stateOptionsLoading = signal(false); + private readonly countrySearch$ = new Subject(); + private readonly stateSearch$ = new Subject(); + readonly columns = signal[]>([ - { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '90px' }, - { key: 'name', label: 'City Name', header: 'City Name', sortable: true, headerAlign: 'center', align: 'left' }, - { key: 'code', label: 'Code', header: 'Code', sortable: true, headerAlign: 'center', align: 'center', badge: true, badgeClass: value => value ? 'badge bg-primary/10 text-primary' : 'badge bg-secondary/10 text-secondary' }, - { key: 'stateName', label: 'State', header: 'State', sortable: true, headerAlign: 'center', align: 'left' }, - { key: 'countryName', label: 'Country', header: 'Country', sortable: true, headerAlign: 'center', align: 'center' }, + { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '8%' }, + { key: 'name', label: 'City Name', header: 'City Name', sortable: true, headerAlign: 'left', align: 'left', width: '30%' }, + //{ key: 'code', label: 'Code', header: 'Code', sortable: true, headerAlign: 'center', align: 'center', badge: true, badgeClass: value => value ? 'badge bg-primary/10 text-primary' : 'badge bg-secondary/10 text-secondary' }, + { key: 'stateName', label: 'State', header: 'State', sortable: true, headerAlign: 'left', align: 'left', width: '22%' }, + { key: 'countryName', label: 'Country', header: 'Country', sortable: true, headerAlign: 'left', align: 'left', width: '22%' }, { - key: 'isActive', label: 'Status', header: 'Status', sortable: true, badge: true, + key: 'isActive', label: 'Status', header: 'Status', sortable: true, badge: true, headerAlign: 'left', align: 'left', width: '18%', badgeClass: value => value === true ? 'badge bg-success/10 text-success' : 'badge bg-danger/10 text-danger', formatter: value => value ? 'Active' : 'Inactive' } @@ -113,25 +112,57 @@ export class CityList implements OnInit { } ]); - readonly filterCountrySearchFn: AutocompleteSearchFn = (term, page) => - this.countryApi.autocomplete(term, page); - readonly filterCountryValueFn: AutocompleteValueFn = c => c.id; - readonly filterCountryDisplayFn: AutocompleteDisplayFn = c => c.name; - - readonly filterStateSearchFn: AutocompleteSearchFn = (term, page) => { - const countryId = this.filterForm.controls.countryId.value || this.selectedCountry()?.id || ''; - if (!countryId) return of([]); - return this.stateApi.autocomplete(countryId, term || '', page); - }; - readonly filterStateValueFn: AutocompleteValueFn = s => s.id; - readonly filterStateDisplayFn: AutocompleteDisplayFn = s => s.name; - ngOnInit(): void { + this.countrySearch$.pipe( + debounceTime(300), + distinctUntilChanged(), + switchMap(term => { + this.countryOptionsLoading.set(true); + return this.countryApi.autocomplete(term, 50); + }), + takeUntilDestroyed(this.destroyRef) + ).subscribe(countries => { + this.countryOptionsLoading.set(false); + this.countryOptions.set(countries.map(country => ({ value: country.id, label: country.name }))); + }); + this.countrySearch$.next(''); + + this.stateSearch$.pipe( + debounceTime(300), + map(term => ({ term, countryIds: this.filterForm.controls.countryId.value })), + distinctUntilChanged((previous, current) => + previous.term === current.term && previous.countryIds.join(',') === current.countryIds.join(',') + ), + switchMap(({ term, countryIds }) => { + if (!countryIds.length) return of([]); + this.stateOptionsLoading.set(true); + return this.stateApi.autocomplete(countryIds, term, 50); + }), + takeUntilDestroyed(this.destroyRef) + ).subscribe(states => { + this.stateOptionsLoading.set(false); + this.stateOptions.set(states.map(state => ({ value: state.id, label: state.name }))); + }); + + this.filterForm.controls.countryId.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(countryIds => { + this.filterForm.controls.stateId.setValue([], { emitEvent: false }); + this.stateOptions.set([]); + if (countryIds.length) { + this.filterForm.controls.stateId.enable({ emitEvent: false }); + this.stateSearch$.next(''); + } else { + this.filterForm.controls.stateId.disable({ emitEvent: false }); + } + }); + this.tableStore.initialize({ + defaultSort: { column: 'name' }, fetcher: query => { - const countryId = this.filterForm.controls.countryId.value || null; - const stateId = this.filterForm.controls.stateId.value || null; - return this.cityApi.getCityDataTable(query, countryId, stateId); + const countryIds = this.filterForm.controls.countryId.value; + const stateIds = this.filterForm.controls.stateId.value; + return this.cityApi.getCityDataTable(query, countryIds, stateIds); }, mapRow: (city, serialNumber) => { const resolvedStateName = city.stateName @@ -160,20 +191,12 @@ export class CityList implements OnInit { }); } - onFilterCountryChanged(country: CountryLookupDto | null): void { - this.selectedCountry.set(country); - this.filterForm.controls.countryId.setValue(country ? country.id : ''); - this.filterForm.controls.stateId.setValue(''); - this.selectedFilterState.set(null); - if (country) { - this.filterForm.controls.stateId.enable(); - } else { - this.filterForm.controls.stateId.disable(); - } + onCountrySearchChanged(term: string): void { + this.countrySearch$.next(term); } - onFilterStateChanged(state: StateLookupDto | null): void { - this.selectedFilterState.set(state); + onStateSearchChanged(term: string): void { + this.stateSearch$.next(term); } onApplyFilter(event?: Event): void { @@ -186,10 +209,9 @@ export class CityList implements OnInit { } onResetFilter(): void { - this.filterForm.reset({ countryId: '', stateId: '' }); + this.filterForm.reset({ countryId: [], stateId: [] }); this.filterForm.controls.stateId.disable(); - this.selectedCountry.set(null); - this.selectedFilterState.set(null); + this.stateOptions.set([]); this.tableStore.reset(); } diff --git a/src/app/features/global-masters/countries/data-access/country.service.ts b/src/app/features/global-masters/countries/data-access/country.service.ts index 0e9f8173..f5d551b8 100644 --- a/src/app/features/global-masters/countries/data-access/country.service.ts +++ b/src/app/features/global-masters/countries/data-access/country.service.ts @@ -21,8 +21,11 @@ import { COUNTRY_ENDPOINTS } from './country.endpoints'; export class CountryService { private readonly http = inject(HttpClient); - getCountryDataTable(query: DataTableQuery, regionId: string | null = null): Observable> { - const params = regionId ? new HttpParams().set('regionId', regionId) : undefined; + getCountryDataTable(query: DataTableQuery, regionIds: readonly string[] = []): Observable> { + let params = new HttpParams(); + for (const regionId of regionIds) { + params = params.append('regionIds', regionId); + } return this.http.post>(COUNTRY_ENDPOINTS.dataTable, query, { params }); } diff --git a/src/app/features/global-masters/countries/pages/country-list/country-list.html b/src/app/features/global-masters/countries/pages/country-list/country-list.html index 46f0f2dd..fc7003be 100644 --- a/src/app/features/global-masters/countries/pages/country-list/country-list.html +++ b/src/app/features/global-masters/countries/pages/country-list/country-list.html @@ -5,6 +5,8 @@ [totalRecords]="tableStore.totalRecords()" [pageIndex]="tableStore.queryState.pageIndex()" [pageSize]="tableStore.queryState.pageSize()" + [initialSortColumn]="tableStore.queryState.sortColumn()" + [initialSortDirection]="tableStore.queryState.sortDirection()" tableTitle="Countries" buttonTitle="Add" [showSearch]="true" @@ -25,11 +27,10 @@
- +
} - {{ value }} + {{ value }}
diff --git a/src/app/features/global-masters/countries/pages/country-list/country-list.ts b/src/app/features/global-masters/countries/pages/country-list/country-list.ts index 828d3f79..e7f668f1 100644 --- a/src/app/features/global-masters/countries/pages/country-list/country-list.ts +++ b/src/app/features/global-masters/countries/pages/country-list/country-list.ts @@ -1,12 +1,13 @@ import { Component, DestroyRef, OnInit, inject, signal, viewChild } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; -import { finalize, map } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { debounceTime, distinctUntilChanged, finalize, switchMap } from 'rxjs/operators'; import { NotificationService } from '../../../../../core/services/common/notification.service'; import { CountryDto, UpdateCountryRequest } from '../../models/country.model'; import { CountryService } from '../../data-access/country.service'; -import { RegionLookupDto, RegionService } from '../../../regions/public-api'; +import { RegionService } from '../../../regions/public-api'; import { DataTable, DataTableToolbarDirective } from '../../../../../shared/components/data-table/data-table'; import { DataTableStore } from '../../../../../shared/components/data-table/data-table.store'; import { @@ -16,13 +17,8 @@ import { DataTableRecord } from '../../../../../shared/components/data-table/data-table.types'; import { DataTableCellDirective } from '../../../../../shared/directives/data-table-cell.directive'; -import { Autocomplete } from '../../../../../shared/components/form/autocomplete/autocomplete'; -import { - AutocompleteDisplayFn, - AutocompleteResolveValueFn, - AutocompleteSearchFn, - AutocompleteValueFn -} from '../../../../../shared/components/form/autocomplete/autocomplete.types'; +import { FormSelect } from '../../../../../shared/components/form/form-select/form-select'; +import { FormSelectOption } from '../../../../../shared/components/form/models/form-select.models'; import { ConfirmDialog } from '../../../../../shared/components/confirm-dialog/confirm-dialog'; import { Button as AppButton } from '../../../../../shared/components/button/button'; import { CountryFormModalComponent } from '../../components/country-form-modal/country-form-modal'; @@ -51,7 +47,7 @@ interface CountryTableRow extends DataTableRecord { DataTableToolbarDirective, DataTableCellDirective, ReactiveFormsModule, - Autocomplete, + FormSelect, ConfirmDialog, AppButton, CountryFormModalComponent @@ -74,41 +70,35 @@ export class CountryList implements OnInit { readonly deleteConfirmDialog = viewChild(ConfirmDialog); readonly showFilters = signal(false); - readonly selectedRegionLookup = signal(null); readonly regionFilterForm = this.formBuilder.nonNullable.group({ - regionId: [''] + regionId: [[] as string[]] }); - readonly searchRegions: AutocompleteSearchFn = (term, limit) => - this.regionApi.autocomplete(term, limit); - readonly displayRegion: AutocompleteDisplayFn = region => region.name; - readonly regionValue: AutocompleteValueFn = region => region.id; - readonly resolveRegion: AutocompleteResolveValueFn = value => - this.regionApi.getRegionById(value).pipe( - map(region => ({ id: region.id, code: region.code, name: region.name })) - ); + readonly regionOptions = signal[]>([]); + readonly regionOptionsLoading = signal(false); + private readonly regionSearch$ = new Subject(); readonly columns = signal[]>([ - { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '90px' }, - { key: 'name', label: 'Name', header: 'Name', sortable: true, headerAlign: 'center', align: 'left' }, - { key: 'iso2', label: 'ISO2', header: 'ISO2', sortable: true, headerAlign: 'center', align: 'center', width: '90px', badge: true, + { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '6%' }, + { key: 'name', label: 'Name', header: 'Name', sortable: true, headerAlign: 'left', align: 'left', width: '24%' }, + { key: 'iso2', label: 'ISO2', header: 'ISO2', sortable: true, headerAlign: 'center', align: 'center', width: '10%', badge: true, badgeClass: value => value ? 'badge bg-primary/10 text-primary' : 'badge bg-secondary/10 text-secondary' }, - { key: 'iso3', label: 'ISO3', header: 'ISO3', sortable: true, headerAlign: 'center', align: 'center', width: '90px', badge: true, + { key: 'iso3', label: 'ISO3', header: 'ISO3', sortable: true, headerAlign: 'center', align: 'center', width: '10%', badge: true, badgeClass: value => value ? 'badge bg-primary/10 text-primary' : 'badge bg-secondary/10 text-secondary' }, { - key: 'phoneCode', label: 'Phone Code', header: 'Phone Code', sortable: true, headerAlign: 'center', align: 'center', badge: true, + key: 'phoneCode', label: 'Phone Code', header: 'Phone Code', sortable: true, headerAlign: 'center', align: 'center', width: '13%', badge: true, badgeClass: value => value ? 'badge bg-primary/10 text-primary' : '', formatter: value => (typeof value === 'string' && value.trim().length > 0) ? value : '—' }, { - key: 'regionName', label: 'Region', header: 'Region', sortable: false, headerAlign: 'center', align: 'center', + key: 'regionName', label: 'Region', header: 'Region', sortable: false, headerAlign: 'center', align: 'center', width: '18%', formatter: value => (typeof value === 'string' && value.trim().length > 0) ? value : '—' }, { - key: 'isActive', label: 'Status', header: 'Status', sortable: true, badge: true, + key: 'isActive', label: 'Status', header: 'Status', sortable: true, badge: true, headerAlign: 'center', align: 'center', width: '11%', badgeClass: value => value === true ? 'badge bg-success/10 text-success' : 'badge bg-danger/10 text-danger', formatter: value => value ? 'Active' : 'Inactive' } @@ -131,17 +121,31 @@ export class CountryList implements OnInit { ]); ngOnInit(): void { + this.regionSearch$.pipe( + debounceTime(300), + distinctUntilChanged(), + switchMap(term => { + this.regionOptionsLoading.set(true); + return this.regionApi.autocomplete(term, 50); + }), + takeUntilDestroyed(this.destroyRef) + ).subscribe(regions => { + this.regionOptionsLoading.set(false); + this.regionOptions.set(regions.map(region => ({ value: region.id, label: region.name }))); + }); + this.regionSearch$.next(''); + this.tableStore.initialize({ + defaultSort: { column: 'name' }, fetcher: query => { - const regionId = this.regionFilterForm.controls.regionId.value || null; - return this.countryApi.getCountryDataTable(query, regionId); + const regionIds = this.regionFilterForm.controls.regionId.value; + return this.countryApi.getCountryDataTable(query, regionIds); } }); } - onFilterRegionSelected(region: RegionLookupDto | null): void { - this.selectedRegionLookup.set(region); - this.regionFilterForm.controls.regionId.setValue(region ? region.id : ''); + onRegionSearchChanged(term: string): void { + this.regionSearch$.next(term); } onApplyFilter(): void { @@ -149,8 +153,7 @@ export class CountryList implements OnInit { } onResetFilter(): void { - this.regionFilterForm.reset({ regionId: '' }); - this.selectedRegionLookup.set(null); + this.regionFilterForm.reset({ regionId: [] }); this.tableStore.reset(); } diff --git a/src/app/features/global-masters/currencies/pages/currency-list/currency-list.html b/src/app/features/global-masters/currencies/pages/currency-list/currency-list.html index f32b068e..c4ecbd75 100644 --- a/src/app/features/global-masters/currencies/pages/currency-list/currency-list.html +++ b/src/app/features/global-masters/currencies/pages/currency-list/currency-list.html @@ -5,6 +5,8 @@ [totalRecords]="tableStore.totalRecords()" [pageIndex]="tableStore.queryState.pageIndex()" [pageSize]="tableStore.queryState.pageSize()" + [initialSortColumn]="tableStore.queryState.sortColumn()" + [initialSortDirection]="tableStore.queryState.sortDirection()" tableTitle="Currencies Management" buttonTitle="Add" [showSearch]="true" diff --git a/src/app/features/global-masters/currencies/pages/currency-list/currency-list.ts b/src/app/features/global-masters/currencies/pages/currency-list/currency-list.ts index ead46ea4..0f62a35f 100644 --- a/src/app/features/global-masters/currencies/pages/currency-list/currency-list.ts +++ b/src/app/features/global-masters/currencies/pages/currency-list/currency-list.ts @@ -84,14 +84,15 @@ export class CurrencyList implements OnInit { private iso2TooltipCloseTimer: ReturnType | null = null; readonly columns = signal[]>([ - { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '90px' }, - { key: 'code', label: 'Code', header: 'Code', sortable: true, headerAlign: 'center', align: 'center', width: '90px' }, - { key: 'name', label: 'Name', header: 'Name', sortable: true, headerAlign: 'center', align: 'left' }, - { key: 'iso2', label: 'Countries', header: 'Countries', sortable: false, headerAlign: 'center', align: 'left' }, - { key: 'symbol', label: 'Symbol', header: 'Symbol', sortable: false, headerAlign: 'center', align: 'center', width: '90px' }, - { key: 'numericCode', label: 'Numeric Code', header: 'Numeric Code', sortable: true, headerAlign: 'center', align: 'center', width: '130px' }, + { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '6%' }, + { key: 'code', label: 'Code', header: 'Code', sortable: true, headerAlign: 'center', align: 'center', width: '9%' }, + { key: 'name', label: 'Name', header: 'Name', sortable: true, headerAlign: 'left', align: 'left', width: '28%' }, + { key: 'iso2', label: 'Countries', header: 'Countries', sortable: false, headerAlign: 'left', align: 'left', width: '17%' }, + { key: 'symbol', label: 'Symbol', header: 'Symbol', sortable: false, headerAlign: 'center', align: 'center', width: '9%' }, + { key: 'numericCode', label: 'Numeric Code', header: 'Numeric Code', sortable: true, headerAlign: 'center', align: 'center', width: '13%' }, { key: 'isActive', label: 'Status', header: 'Status', sortable: true, badge: true, + headerAlign: 'center', align: 'center', width: '11%', badgeClass: value => value === true ? 'badge bg-success/10 text-success' : 'badge bg-danger/10 text-danger', formatter: value => value ? 'Active' : 'Inactive' } @@ -114,9 +115,12 @@ export class CurrencyList implements OnInit { ]); ngOnInit(): void { + this.tableStore.initialize({ + defaultSort: { column: 'code' }, fetcher: query => this.currencyApi.getCurrencyDataTable(query) }); + } onAddCurrency(): void { diff --git a/src/app/features/global-masters/exchange-rates/pages/exchange-rate-list/exchange-rate-list.html b/src/app/features/global-masters/exchange-rates/pages/exchange-rate-list/exchange-rate-list.html index c15380e6..67d26099 100644 --- a/src/app/features/global-masters/exchange-rates/pages/exchange-rate-list/exchange-rate-list.html +++ b/src/app/features/global-masters/exchange-rates/pages/exchange-rate-list/exchange-rate-list.html @@ -1,6 +1,8 @@ - + {{ getCurrencyDisplay(row.fromCurrencyId, value) }} - + {{ getCurrencyDisplay(row.toCurrencyId, value) }} - + {{ value | number:'1.4-4' }} diff --git a/src/app/features/global-masters/exchange-rates/pages/exchange-rate-list/exchange-rate-list.ts b/src/app/features/global-masters/exchange-rates/pages/exchange-rate-list/exchange-rate-list.ts index 55753b22..788e6971 100644 --- a/src/app/features/global-masters/exchange-rates/pages/exchange-rate-list/exchange-rate-list.ts +++ b/src/app/features/global-masters/exchange-rates/pages/exchange-rate-list/exchange-rate-list.ts @@ -87,15 +87,15 @@ export class ExchangeRateList implements OnInit { }); readonly columns = signal[]>([ - { key: 'serialNumber', label: 'Sr. No.', header: 'Sr. No.', sortable: false, width: '80px', align: 'center', headerAlign: 'center' }, - { key: 'fromCurrencyCode', label: 'From', header: 'From', sortable: true, headerAlign: 'center', align: 'center', width:'200px'}, - { key: 'toCurrencyCode', label: 'To', header: 'To', sortable: true, headerAlign: 'center', align: 'center', width:'200px'}, - { key: 'rate', label: 'Rate', header: 'Rate', sortable: true, headerAlign: 'right', align: 'right' }, - { key: 'rateType', label: 'Type', header: 'Type', sortable: true, headerAlign: 'center', align: 'center' }, - { key: 'source', label: 'Source', header: 'Source', sortable: true, headerAlign: 'center', align: 'center' }, - { key: 'effectiveFrom', label: 'Effective From', header: 'Effective From', sortable: true, headerAlign: 'center', align: 'center'}, - { key: 'effectiveTo', label: 'Effective To', header: 'Effective To', sortable: true, headerAlign: 'center', align: 'center' }, - { key: 'isActive', label: 'Status', header: 'Status', sortable: true, headerAlign: 'center', align: 'center' } + { key: 'serialNumber', label: 'Sr. No.', header: 'Sr. No.', sortable: false, width: '6%', align: 'center', headerAlign: 'center' }, + { key: 'fromCurrencyCode', label: 'From', header: 'From', sortable: true, headerAlign: 'center', align: 'center', width: '10%' }, + { key: 'toCurrencyCode', label: 'To', header: 'To', sortable: true, headerAlign: 'center', align: 'center', width: '10%' }, + { key: 'rate', label: 'Rate', header: 'Rate', sortable: true, headerAlign: 'right', align: 'right', width: '11%' }, + { key: 'rateType', label: 'Type', header: 'Type', sortable: true, headerAlign: 'center', align: 'center', width: '11%' }, + { key: 'source', label: 'Source', header: 'Source', sortable: true, headerAlign: 'center', align: 'center', width: '11%' }, + { key: 'effectiveFrom', label: 'Effective From', header: 'Effective From', sortable: true, headerAlign: 'center', align: 'center', width: '13%' }, + { key: 'effectiveTo', label: 'Effective To', header: 'Effective To', sortable: true, headerAlign: 'center', align: 'center', width: '13%' }, + { key: 'isActive', label: 'Status', header: 'Status', sortable: true, headerAlign: 'center', align: 'center', width: '11%' } ]); readonly actions = signal[]>([ @@ -114,6 +114,7 @@ export class ExchangeRateList implements OnInit { this.loadCurrencyLookup(); this.tableStore.initialize({ + defaultSort: { column: 'fromCurrencyCode' }, fetcher: query => { const rType = this.filterForm.controls.rateType.value; const fullQuery = { diff --git a/src/app/features/global-masters/languages/pages/language-list/language-list.html b/src/app/features/global-masters/languages/pages/language-list/language-list.html index edc48aab..70f9514e 100644 --- a/src/app/features/global-masters/languages/pages/language-list/language-list.html +++ b/src/app/features/global-masters/languages/pages/language-list/language-list.html @@ -5,6 +5,8 @@ [totalRecords]="tableStore.totalRecords()" [pageIndex]="tableStore.queryState.pageIndex()" [pageSize]="tableStore.queryState.pageSize()" + [initialSortColumn]="tableStore.queryState.sortColumn()" + [initialSortDirection]="tableStore.queryState.sortDirection()" tableTitle="Languages" buttonTitle="Add" [showSearch]="true" diff --git a/src/app/features/global-masters/languages/pages/language-list/language-list.ts b/src/app/features/global-masters/languages/pages/language-list/language-list.ts index c4aa87c6..dc52b00e 100644 --- a/src/app/features/global-masters/languages/pages/language-list/language-list.ts +++ b/src/app/features/global-masters/languages/pages/language-list/language-list.ts @@ -48,17 +48,17 @@ export class LanguageList implements OnInit { readonly deleteConfirmDialog = viewChild(ConfirmDialog); readonly columns = signal[]>([ - { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '100px' }, - { key: 'name', label: 'Language Name', header: 'Language Name', sortable: true, align: 'left' }, - { key: 'code', label: 'Language Code', header: 'Language Code', sortable: true }, - { key: 'nativeName', label: 'Native Name', header: 'Native Name', sortable: true }, + { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '6%' }, + { key: 'name', label: 'Language Name', header: 'Language Name', sortable: true, align: 'left', headerAlign: 'left', width: '28%' }, + { key: 'code', label: 'Language Code', header: 'Language Code', sortable: true, headerAlign: 'center', align: 'center', width: '18%' }, + { key: 'nativeName', label: 'Native Name', header: 'Native Name', sortable: true, headerAlign: 'center', align: 'center', width: '22%' }, { - key: 'isRightToLeft', label: 'RTL', header: 'RTL', sortable: false, badge: true, + key: 'isRightToLeft', label: 'RTL', header: 'RTL', sortable: false, badge: true, headerAlign: 'center', align: 'center', width: '10%', badgeClass: value => value === true ? 'badge bg-info/10 text-info' : 'badge bg-light text-defaulttextcolor', formatter: value => value ? 'Yes' : 'No' }, { - key: 'isActive', label: 'Status', header: 'Status', sortable: true, badge: true, + key: 'isActive', label: 'Status', header: 'Status', sortable: true, badge: true, headerAlign: 'center', align: 'center', width: '11%', badgeClass: value => value === true ? 'badge bg-success/10 text-success' : 'badge bg-danger/10 text-danger', formatter: value => value ? 'Active' : 'Inactive' } @@ -82,6 +82,7 @@ export class LanguageList implements OnInit { ngOnInit(): void { this.tableStore.initialize({ + defaultSort: { column: 'name' }, fetcher: query => this.languageApi.getDataTable(query) }); } diff --git a/src/app/features/global-masters/plans/pages/plan-list/plan-list.html b/src/app/features/global-masters/plans/pages/plan-list/plan-list.html index 0756aac3..df65cf78 100644 --- a/src/app/features/global-masters/plans/pages/plan-list/plan-list.html +++ b/src/app/features/global-masters/plans/pages/plan-list/plan-list.html @@ -5,6 +5,8 @@ [totalRecords]="tableStore.totalRecords()" [pageIndex]="tableStore.queryState.pageIndex()" [pageSize]="tableStore.queryState.pageSize()" + [initialSortColumn]="tableStore.queryState.sortColumn()" + [initialSortDirection]="tableStore.queryState.sortDirection()" tableTitle="Plans" buttonTitle="Add" [showSearch]="true" diff --git a/src/app/features/global-masters/plans/pages/plan-list/plan-list.ts b/src/app/features/global-masters/plans/pages/plan-list/plan-list.ts index 003ebb46..f425ac0a 100644 --- a/src/app/features/global-masters/plans/pages/plan-list/plan-list.ts +++ b/src/app/features/global-masters/plans/pages/plan-list/plan-list.ts @@ -41,23 +41,23 @@ export class PlanList implements OnInit { readonly deleteConfirmDialog = viewChild(ConfirmDialog); readonly columns = signal[]>([ - { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '90px' }, - { key: 'name', label: 'Name', header: 'Name', sortable: true, headerAlign: 'center', align: 'left' }, - { key: 'code', label: 'Code', header: 'Code', sortable: true, headerAlign: 'center', align: 'center', badge: true, badgeClass: () => 'badge bg-primary/10 text-primary' }, + { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '5%' }, + { key: 'name', label: 'Name', header: 'Name', sortable: true, headerAlign: 'left', align: 'left', width: '15%' }, + { key: 'code', label: 'Code', header: 'Code', sortable: true, headerAlign: 'left', align: 'left', width: '8%', badge: true, badgeClass: () => 'badge bg-primary/10 text-primary' }, { - key: 'price', label: 'Price', header: 'Price', sortable: true, headerAlign: 'center', align: 'right', + key: 'price', label: 'Price', header: 'Price', sortable: true, headerAlign: 'center', align: 'center', width: '8%', formatter: value => Number(value).toFixed(2) }, { - key: 'billingCycle', label: 'Billing Cycle', header: 'Billing Cycle', sortable: true, headerAlign: 'center', align: 'center', + key: 'billingCycle', label: 'Billing Cycle', header: 'Billing Cycle', sortable: true, headerAlign: 'center', align: 'center', width: '10%', formatter: value => BILLING_CYCLE_LABELS[value as BillingCycle] ?? '—' }, - { key: 'maxCompanies', label: 'Max Companies', header: 'Max Companies', sortable: true, headerAlign: 'center', align: 'center' }, - { key: 'maxUsers', label: 'Max Users', header: 'Max Users', sortable: true, headerAlign: 'center', align: 'center' }, - { key: 'maxStorageGb', label: 'Max Storage (GB)', header: 'Max Storage (GB)', sortable: true, headerAlign: 'center', align: 'center' }, - { key: 'defaultTrialDays', label: 'Trial Days', header: 'Trial Days', sortable: true, headerAlign: 'center', align: 'center' }, + { key: 'maxCompanies', label: 'Max Companies', header: 'Max Companies', sortable: true, headerAlign: 'center', align: 'center', width: '9%' }, + { key: 'maxUsers', label: 'Max Users', header: 'Max Users', sortable: true, headerAlign: 'center', align: 'center', width: '9%' }, + { key: 'maxStorageGb', label: 'Max Storage (GB)', header: 'Max Storage (GB)', sortable: true, headerAlign: 'center', align: 'center', width: '10%' }, + { key: 'defaultTrialDays', label: 'Trial Days', header: 'Trial Days', sortable: true, headerAlign: 'center', align: 'center', width: '9%' }, { - key: 'isActive', label: 'Status', header: 'Status', sortable: true, badge: true, + key: 'isActive', label: 'Status', header: 'Status', sortable: true, badge: true, headerAlign: 'center', align: 'center', width: '9%', badgeClass: value => value === true ? 'badge bg-success/10 text-success' : 'badge bg-danger/10 text-danger', formatter: value => value ? 'Active' : 'Inactive' } @@ -81,6 +81,7 @@ export class PlanList implements OnInit { ngOnInit(): void { this.tableStore.initialize({ + defaultSort: { column: 'name' }, fetcher: query => this.planApi.getPlanDataTable(query), mapRow: (plan, serialNumber) => ({ id: plan.id, diff --git a/src/app/features/global-masters/regions/pages/region-list/region-list.html b/src/app/features/global-masters/regions/pages/region-list/region-list.html index 18e43887..25ee629f 100644 --- a/src/app/features/global-masters/regions/pages/region-list/region-list.html +++ b/src/app/features/global-masters/regions/pages/region-list/region-list.html @@ -5,6 +5,8 @@ [totalRecords]="tableStore.totalRecords()" [pageIndex]="tableStore.queryState.pageIndex()" [pageSize]="tableStore.queryState.pageSize()" + [initialSortColumn]="tableStore.queryState.sortColumn()" + [initialSortDirection]="tableStore.queryState.sortDirection()" tableTitle="Regions" buttonTitle="Add" [showSearch]="true" diff --git a/src/app/features/global-masters/regions/pages/region-list/region-list.ts b/src/app/features/global-masters/regions/pages/region-list/region-list.ts index e78af58a..d3b4733c 100644 --- a/src/app/features/global-masters/regions/pages/region-list/region-list.ts +++ b/src/app/features/global-masters/regions/pages/region-list/region-list.ts @@ -47,17 +47,17 @@ export class RegionList implements OnInit { readonly deleteConfirmDialog = viewChild(ConfirmDialog); readonly columns = signal[]>([ - { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '90px' }, - { key: 'name', label: 'Name', header: 'Name', sortable: true, headerAlign: 'center', align: 'left' }, - { key: 'code', label: 'Code', header: 'Code', sortable: true, headerAlign: 'center', align: 'center', width: '110px', badge: true, + { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '7%' }, + { key: 'name', label: 'Name', header: 'Name', sortable: true, headerAlign: 'left', align: 'left', width: '40%' }, + { key: 'code', label: 'Code', header: 'Code', sortable: true, headerAlign: 'center', align: 'center', width: '16%', badge: true, badgeClass: value => value ? 'badge bg-primary/10 text-primary' : 'badge bg-secondary/10 text-secondary' }, { - key: 'countryCount', label: 'Countries', header: 'Countries', sortable: false, headerAlign: 'center', align: 'center', width: '110px', + key: 'countryCount', label: 'Countries', header: 'Countries', sortable: false, headerAlign: 'center', align: 'center', width: '16%', formatter: value => (typeof value === 'number' ? value : 0).toString() }, { - key: 'isActive', label: 'Status', header: 'Status', sortable: true, badge: true, + key: 'isActive', label: 'Status', header: 'Status', sortable: true, badge: true, headerAlign: 'center', align: 'center', width: '13%', badgeClass: value => value === true ? 'badge bg-success/10 text-success' : 'badge bg-danger/10 text-danger', formatter: value => value ? 'Active' : 'Inactive' } @@ -81,6 +81,7 @@ export class RegionList implements OnInit { ngOnInit(): void { this.tableStore.initialize({ + defaultSort: { column: 'name' }, fetcher: query => this.regionApi.getRegionDataTable(query) }); } diff --git a/src/app/features/global-masters/states/data-access/state.service.ts b/src/app/features/global-masters/states/data-access/state.service.ts index 9bee3f5c..dc9f3c40 100644 --- a/src/app/features/global-masters/states/data-access/state.service.ts +++ b/src/app/features/global-masters/states/data-access/state.service.ts @@ -18,8 +18,11 @@ export class StateService { private readonly http = inject(HttpClient); - getStateDataTable(query: DataTableQuery, countryId: string | null = null): Observable> { - const params = countryId ? new HttpParams().set('countryId', countryId) : undefined; + getStateDataTable(query: DataTableQuery, countryIds: readonly string[] = []): Observable> { + let params = new HttpParams(); + for (const countryId of countryIds) { + params = params.append('countryIds', countryId); + } return this.http.post>(`${STATE_ENDPOINTS.dataTable}`, query, { params }); } @@ -43,12 +46,12 @@ export class StateService { return this.http.get(STATE_ENDPOINTS.getById(id)); } - autocomplete(countryId: string, term = '', limit = 50): Observable { - return this.http.get(STATE_ENDPOINTS.autocomplete, { - params: new HttpParams() - .set('countryId', countryId) - .set('term', term) - .set('limit', limit) - }); + autocomplete(countryIds: string | readonly string[], term = '', limit = 50): Observable { + let params = new HttpParams().set('term', term).set('limit', limit); + const ids = Array.isArray(countryIds) ? countryIds : [countryIds]; + for (const countryId of ids) { + params = params.append('countryIds', countryId); + } + return this.http.get(STATE_ENDPOINTS.autocomplete, { params }); } } diff --git a/src/app/features/global-masters/states/pages/state-list/state-list.html b/src/app/features/global-masters/states/pages/state-list/state-list.html index e0acaa24..3a465b43 100644 --- a/src/app/features/global-masters/states/pages/state-list/state-list.html +++ b/src/app/features/global-masters/states/pages/state-list/state-list.html @@ -1,6 +1,8 @@
- +
); - readonly selectedCountryLookup = signal(null); readonly statusChangingId = signal(null); readonly deletingId = signal(null); readonly pendingDeleteState = signal(null); @@ -71,28 +65,26 @@ export class StateList implements OnInit { readonly showFilters = signal(false); readonly countryFilterForm = this.formBuilder.nonNullable.group({ - countryId: [''] + countryId: [[] as string[]] }); - readonly searchCountries: AutocompleteSearchFn = (term, limit) => - this.countryApi.autocomplete(term, limit); - readonly displayCountry: AutocompleteDisplayFn = country => country.name; - readonly countryValue: AutocompleteValueFn = country => country.id; - readonly resolveCountry: AutocompleteResolveValueFn = value => - this.countryApi.getCountryById(value).pipe( - map(country => ({ id: country.id, iso2: country.iso2, name: country.name })) - ); + readonly countryOptions = signal[]>([]); + readonly countryOptionsLoading = signal(false); + private readonly countrySearch$ = new Subject(); readonly columns = signal[]>([ - { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '90px' }, - { key: 'name', label: 'State Name', header: 'State Name', sortable: true, headerAlign: 'center', align: 'left' }, + { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '7%' }, + { key: 'name', label: 'State Name', header: 'State Name', sortable: true, headerAlign: 'left', align: 'left', width: '28%' }, { - key: 'code', label: 'Code', header: 'Code', sortable: true, headerAlign: 'center', align: 'center', badge: true, + key: 'code', label: 'Code', header: 'Code', sortable: true, headerAlign: 'center', align: 'center', width: '14%', badge: true, badgeClass: value => value ? 'badge bg-primary/10 text-primary' : 'badge bg-secondary/10 text-secondary', formatter: value => (typeof value === 'string' && value.trim().length > 0) ? value : '—' }, { - key: 'isActive', label: 'Status', header: 'Status', sortable: true, badge: true, + key: 'countryName', label: 'Country', header: 'Country', sortable: true, headerAlign: 'left', align: 'left', width: '28%', + }, + { + key: 'isActive', label: 'Status', header: 'Status', sortable: true, badge: true, headerAlign: 'center', align: 'center', width: '13%', badgeClass: value => value === true ? 'badge bg-success/10 text-success' : 'badge bg-danger/10 text-danger', formatter: value => value ? 'Active' : 'Inactive' } @@ -115,17 +107,31 @@ export class StateList implements OnInit { ]); ngOnInit(): void { + this.countrySearch$.pipe( + debounceTime(300), + distinctUntilChanged(), + switchMap(term => { + this.countryOptionsLoading.set(true); + return this.countryApi.autocomplete(term, 50); + }), + takeUntilDestroyed(this.destroyRef) + ).subscribe(countries => { + this.countryOptionsLoading.set(false); + this.countryOptions.set(countries.map(country => ({ value: country.id, label: country.name }))); + }); + this.countrySearch$.next(''); + this.tableStore.initialize({ + defaultSort: { column: 'name' }, fetcher: query => { - const countryId = this.countryFilterForm.controls.countryId.value || null; - return this.stateApi.getStateDataTable(query, countryId); + const countryIds = this.countryFilterForm.controls.countryId.value; + return this.stateApi.getStateDataTable(query, countryIds); } }); } - onFilterCountrySelected(country: CountryLookupDto | null): void { - this.selectedCountryLookup.set(country); - this.countryFilterForm.controls.countryId.setValue(country ? country.id : ''); + onCountrySearchChanged(term: string): void { + this.countrySearch$.next(term); } onApplyFilter(): void { @@ -133,8 +139,7 @@ export class StateList implements OnInit { } onResetFilter(): void { - this.countryFilterForm.reset({ countryId: '' }); - this.selectedCountryLookup.set(null); + this.countryFilterForm.reset({ countryId: [] }); this.tableStore.reset(); } diff --git a/src/app/features/global-masters/timezones/pages/timezone-list/timezone-list.html b/src/app/features/global-masters/timezones/pages/timezone-list/timezone-list.html index 8a085fe2..2b3c323f 100644 --- a/src/app/features/global-masters/timezones/pages/timezone-list/timezone-list.html +++ b/src/app/features/global-masters/timezones/pages/timezone-list/timezone-list.html @@ -5,6 +5,8 @@ [totalRecords]="tableStore.totalRecords()" [pageIndex]="tableStore.queryState.pageIndex()" [pageSize]="tableStore.queryState.pageSize()" + [initialSortColumn]="tableStore.queryState.sortColumn()" + [initialSortDirection]="tableStore.queryState.sortDirection()" tableTitle="Timezones" buttonTitle="Add" [showSearch]="true" @@ -19,7 +21,7 @@ (actionClicked)="onActionClick($event)" > - {{ value }} + {{ value }} {{ value }} diff --git a/src/app/features/global-masters/timezones/pages/timezone-list/timezone-list.ts b/src/app/features/global-masters/timezones/pages/timezone-list/timezone-list.ts index d511ef15..6546906b 100644 --- a/src/app/features/global-masters/timezones/pages/timezone-list/timezone-list.ts +++ b/src/app/features/global-masters/timezones/pages/timezone-list/timezone-list.ts @@ -60,15 +60,15 @@ export class TimezoneList implements OnInit { readonly deleteConfirmDialog = viewChild(ConfirmDialog); readonly columns = signal[]>([ - { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '90px' }, - { key: 'ianaId', label: 'IANA ID', header: 'IANA ID', sortable: true, headerAlign: 'center', align: 'left' }, - { key: 'displayName', label: 'Display Name', header: 'Display Name', sortable: true, headerAlign: 'center', align: 'left' }, + { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '6%' }, + { key: 'ianaId', label: 'IANA ID', header: 'IANA ID', sortable: true, headerAlign: 'left', align: 'left', width: '26%' }, + { key: 'displayName', label: 'Display Name', header: 'Display Name', sortable: true, headerAlign: 'left', align: 'left', width: '32%' }, { - key: 'utcOffsetMinutes', label: 'UTC Offset', header: 'UTC Offset', sortable: true, + key: 'utcOffsetMinutes', label: 'UTC Offset', header: 'UTC Offset', sortable: true, headerAlign: 'center', align: 'center', width: '18%', formatter: value => this.formatUtcOffset(Number(value)) }, { - key: 'isActive', label: 'Status', header: 'Status', sortable: true, badge: true, + key: 'isActive', label: 'Status', header: 'Status', sortable: true, badge: true, headerAlign: 'center', align: 'center', width: '11%', badgeClass: value => value === true ? 'badge bg-success/10 text-success' : 'badge bg-danger/10 text-danger', @@ -110,6 +110,7 @@ export class TimezoneList implements OnInit { ngOnInit(): void { this.tableStore.initialize({ + defaultSort: { column: 'ianaId' }, fetcher: query => this.timezoneApi.getDataTable(query) }); } diff --git a/src/app/features/localization/translation-keys/pages/translation-key-list/translation-key-list.html b/src/app/features/localization/translation-keys/pages/translation-key-list/translation-key-list.html index 649be08f..72efbb9e 100644 --- a/src/app/features/localization/translation-keys/pages/translation-key-list/translation-key-list.html +++ b/src/app/features/localization/translation-keys/pages/translation-key-list/translation-key-list.html @@ -5,6 +5,8 @@ [totalRecords]="tableStore.totalRecords()" [pageIndex]="tableStore.queryState.pageIndex()" [pageSize]="tableStore.queryState.pageSize()" + [initialSortColumn]="tableStore.queryState.sortColumn()" + [initialSortDirection]="tableStore.queryState.sortDirection()" tableTitle="Translation Keys" buttonTitle="Add" [showSearch]="true" diff --git a/src/app/features/localization/translation-keys/pages/translation-key-list/translation-key-list.ts b/src/app/features/localization/translation-keys/pages/translation-key-list/translation-key-list.ts index a2eefeeb..aeee9d7c 100644 --- a/src/app/features/localization/translation-keys/pages/translation-key-list/translation-key-list.ts +++ b/src/app/features/localization/translation-keys/pages/translation-key-list/translation-key-list.ts @@ -58,8 +58,8 @@ export class TranslationKeyList implements OnInit { }); readonly columns = signal[]>([ - { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '80px' }, - { key: 'keyName', label: 'Key Name', header: 'Key Name', sortable: true, align: 'left', headerAlign: 'left' }, + { key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '6%' }, + { key: 'keyName', label: 'Key Name', header: 'Key Name', sortable: true, align: 'left', headerAlign: 'left', width: '16%' }, { key: 'moduleCode', label: 'Module Code', @@ -67,12 +67,13 @@ export class TranslationKeyList implements OnInit { sortable: true, align: 'left', headerAlign: 'left', + width: '12%', badge: true, badgeClass: value => value && value !== 'Global' ? 'badge bg-primary/10 text-primary' : 'badge bg-gray-100 text-gray-600' }, - { key: 'componentType', label: 'Component Type', header: 'Component Type', sortable: true, align: 'left', headerAlign: 'left' }, - { key: 'defaultText', label: 'Default Text', header: 'Default Text', sortable: true, align: 'left', headerAlign: 'left' }, - { key: 'description', label: 'Description', header: 'Description', sortable: false, align: 'left', headerAlign: 'left' } + { key: 'componentType', label: 'Component Type', header: 'Component Type', sortable: true, align: 'left', headerAlign: 'left', width: '14%' }, + { key: 'defaultText', label: 'Default Text', header: 'Default Text', sortable: true, align: 'left', headerAlign: 'left', width: '22%' }, + { key: 'description', label: 'Description', header: 'Description', sortable: false, align: 'left', headerAlign: 'left', width: '20%' } ]); readonly actions = signal[]>([ @@ -88,6 +89,7 @@ export class TranslationKeyList implements OnInit { ngOnInit(): void { this.tableStore.initialize({ + defaultSort: { column: 'keyName' }, fetcher: query => { const filters: TranslationKeyFilterParams = { moduleCode: this.filterForm.controls.moduleCode.value || null, diff --git a/src/app/features/localization/translations/pages/translation-list/translation-list.html b/src/app/features/localization/translations/pages/translation-list/translation-list.html index 3fa1fb98..9108c9a9 100644 --- a/src/app/features/localization/translations/pages/translation-list/translation-list.html +++ b/src/app/features/localization/translations/pages/translation-list/translation-list.html @@ -4,6 +4,8 @@ [totalRecords]="tableStore.totalRecords()" [pageIndex]="tableStore.queryState.pageIndex()" [pageSize]="tableStore.queryState.pageSize()" + [initialSortColumn]="tableStore.queryState.sortColumn()" + [initialSortDirection]="tableStore.queryState.sortDirection()" tableTitle="Translation Manager" buttonTitle="Add" [showSearch]="true" diff --git a/src/app/features/localization/translations/pages/translation-list/translation-list.ts b/src/app/features/localization/translations/pages/translation-list/translation-list.ts index aa62a660..4019ff48 100644 --- a/src/app/features/localization/translations/pages/translation-list/translation-list.ts +++ b/src/app/features/localization/translations/pages/translation-list/translation-list.ts @@ -72,24 +72,27 @@ export class TranslationList implements OnInit { } private buildColumns(languages: readonly LanguageLookupDto[]): void { + const languageColumnWidth = languages.length > 0 ? `${Math.floor(74 / languages.length)}%` : undefined; const languageColumns: DataTableColumn[] = languages.map(lang => ({ key: lang.code, label: lang.name, header: `${lang.name} (${lang.code})`, sortable: false, align: 'left', - headerAlign: 'left' + headerAlign: 'left', + width: languageColumnWidth })); this.columns.set([ - { key: 'serialNumber', label: 'Sr. No.', header: 'Sr. No.', sortable: false, width: '80px', align: 'center', headerAlign: 'center' }, - { key: 'keyName', label: 'Key / Default Text', header: 'Key / Default Text', sortable: true, align: 'left', headerAlign: 'left' }, + { key: 'serialNumber', label: 'Sr. No.', header: 'Sr. No.', sortable: false, width: '6%', align: 'center', headerAlign: 'center' }, + { key: 'keyName', label: 'Key / Default Text', header: 'Key / Default Text', sortable: true, align: 'left', headerAlign: 'left', width: '20%' }, ...languageColumns ]); } private initializeTable(): void { this.tableStore.initialize({ + defaultSort: { column: 'keyName' }, fetcher: query => this.fetchMatrixPage(query), mapRow: (key, serialNumber) => this.mapToRow(key, serialNumber), onError: (err: any) => { diff --git a/src/app/features/organizations/dashboard/dashboard.ts b/src/app/features/organizations/dashboard/dashboard.ts index b3892490..b478c815 100644 --- a/src/app/features/organizations/dashboard/dashboard.ts +++ b/src/app/features/organizations/dashboard/dashboard.ts @@ -190,6 +190,7 @@ export class Dashboard implements OnInit { header: 'Name', sortable: true, align: 'left', + headerAlign: 'left', }, { key: 'country', label: 'Country', header: 'Country', sortable: true }, { diff --git a/src/app/features/organizations/pages/organization-awaiting-db/organization-awaiting-db.html b/src/app/features/organizations/pages/organization-awaiting-db/organization-awaiting-db.html index 9fc2a41e..a27d00e4 100644 --- a/src/app/features/organizations/pages/organization-awaiting-db/organization-awaiting-db.html +++ b/src/app/features/organizations/pages/organization-awaiting-db/organization-awaiting-db.html @@ -6,6 +6,8 @@ [totalRecords]="tableStore.filteredRecords()" [pageIndex]="tableStore.queryState.pageIndex()" [pageSize]="tableStore.queryState.pageSize()" + [initialSortColumn]="tableStore.queryState.sortColumn()" + [initialSortDirection]="tableStore.queryState.sortDirection()" tableTitle="Organizations Awaiting Database" [showSearch]="true" rowColorMode="none" diff --git a/src/app/features/organizations/pages/organization-awaiting-db/organization-awaiting-db.ts b/src/app/features/organizations/pages/organization-awaiting-db/organization-awaiting-db.ts index f493cc5f..39055a02 100644 --- a/src/app/features/organizations/pages/organization-awaiting-db/organization-awaiting-db.ts +++ b/src/app/features/organizations/pages/organization-awaiting-db/organization-awaiting-db.ts @@ -48,6 +48,7 @@ export class OrganizationAwaitingDb implements OnInit { header: 'Organization', sortable: true, align: 'left', + headerAlign: 'left', }, { key: 'country', label: 'Country', header: 'Country', sortable: true }, // { key: 'wizardFinished', label: 'Wizard Finished', header: 'Wizard Finished', sortable: true }, @@ -70,6 +71,7 @@ export class OrganizationAwaitingDb implements OnInit { header: 'DB Connection Name', sortable: true, align: 'left', + headerAlign: 'left', formatter: value => (value && value !== '—' ? String(value) : '—') } ]); @@ -85,6 +87,7 @@ export class OrganizationAwaitingDb implements OnInit { ngOnInit(): void { this.tableStore.initialize({ + defaultSort: { column: 'code' }, fetcher: query => this.awaitingDbApi.getAwaitingDbOrganizations(query), mapRow: (item, serialNumber) => { const resolvedName = item.organizationName || item.name || '—'; diff --git a/src/app/features/organizations/pages/organization-list/organization-list.html b/src/app/features/organizations/pages/organization-list/organization-list.html index 23eac92d..bbb3f3ec 100644 --- a/src/app/features/organizations/pages/organization-list/organization-list.html +++ b/src/app/features/organizations/pages/organization-list/organization-list.html @@ -5,6 +5,8 @@ [totalRecords]="tableStore.filteredRecords()" [pageIndex]="tableStore.queryState.pageIndex()" [pageSize]="tableStore.queryState.pageSize()" + [initialSortColumn]="tableStore.queryState.sortColumn()" + [initialSortDirection]="tableStore.queryState.sortDirection()" tableTitle="Organizations" buttonTitle="Add" [showSearch]="true" diff --git a/src/app/features/organizations/pages/organization-list/organization-list.ts b/src/app/features/organizations/pages/organization-list/organization-list.ts index fa5e2f86..f9f47e5d 100644 --- a/src/app/features/organizations/pages/organization-list/organization-list.ts +++ b/src/app/features/organizations/pages/organization-list/organization-list.ts @@ -73,6 +73,7 @@ export class OrganizationList implements OnInit { header: 'Organization Name', sortable: true, align: 'left', + headerAlign: 'left', }, { key: 'country', label: 'Country', header: 'Country', sortable: true }, { @@ -128,6 +129,7 @@ export class OrganizationList implements OnInit { ngOnInit(): void { this.tableStore.initialize({ + defaultSort: { column: 'code' }, fetcher: query => this.organizationApi.getOrganizationDataTable(query, this.appliedCountryId()), mapRow: (item, serialNumber) => { const statusText = typeof item.status === 'number' diff --git a/src/app/features/organizations/pages/tenant-domain-list/components/tenant-domain-table/tenant-domain-table.html b/src/app/features/organizations/pages/tenant-domain-list/components/tenant-domain-table/tenant-domain-table.html index 40fe634d..c3c29033 100644 --- a/src/app/features/organizations/pages/tenant-domain-list/components/tenant-domain-table/tenant-domain-table.html +++ b/src/app/features/organizations/pages/tenant-domain-list/components/tenant-domain-table/tenant-domain-table.html @@ -5,6 +5,8 @@ [totalRecords]="totalRecords()" [pageIndex]="pageIndex()" [pageSize]="pageSize()" + [initialSortColumn]="sortColumn()" + [initialSortDirection]="sortDirection()" tableTitle="Tenant Domains" buttonTitle="Add" [showSearch]="true" diff --git a/src/app/features/organizations/pages/tenant-domain-list/components/tenant-domain-table/tenant-domain-table.ts b/src/app/features/organizations/pages/tenant-domain-list/components/tenant-domain-table/tenant-domain-table.ts index bb8e247a..8e1e4a25 100644 --- a/src/app/features/organizations/pages/tenant-domain-list/components/tenant-domain-table/tenant-domain-table.ts +++ b/src/app/features/organizations/pages/tenant-domain-list/components/tenant-domain-table/tenant-domain-table.ts @@ -34,6 +34,8 @@ export class TenantDomainTableComponent { readonly totalRecords = input(0); readonly pageIndex = input(1); readonly pageSize = input(10); + readonly sortColumn = input(''); + readonly sortDirection = input<'asc' | 'desc'>('asc'); readonly showFilters = input(false); readonly addClicked = output(); diff --git a/src/app/features/organizations/pages/tenant-domain-list/tenant-domain-list.html b/src/app/features/organizations/pages/tenant-domain-list/tenant-domain-list.html index ba80b2a1..891b70b4 100644 --- a/src/app/features/organizations/pages/tenant-domain-list/tenant-domain-list.html +++ b/src/app/features/organizations/pages/tenant-domain-list/tenant-domain-list.html @@ -5,6 +5,8 @@ [totalRecords]="tableStore.filteredRecords()" [pageIndex]="tableStore.queryState.pageIndex()" [pageSize]="tableStore.queryState.pageSize()" + [sortColumn]="tableStore.queryState.sortColumn()" + [sortDirection]="tableStore.queryState.sortDirection()" [showFilters]="showFilters()" (addClicked)="onAddDomain()" (filterClicked)="onToggleFilters()" diff --git a/src/app/features/organizations/pages/tenant-domain-list/tenant-domain-list.ts b/src/app/features/organizations/pages/tenant-domain-list/tenant-domain-list.ts index fb301b42..db751cb9 100644 --- a/src/app/features/organizations/pages/tenant-domain-list/tenant-domain-list.ts +++ b/src/app/features/organizations/pages/tenant-domain-list/tenant-domain-list.ts @@ -56,8 +56,8 @@ export class TenantDomainList implements OnInit { readonly columns = signal[]>([ { key: 'serialNumber', label: 'Sr. No.', header: 'Sr. No.', sortable: false, width: '80px', align: 'center', headerAlign: 'center' }, - { key: 'domainName', label: 'Domain', header: 'Domain', sortable: true, align: 'left' }, - { key: 'organizationName', label: 'Organization', header: 'Organization', sortable: true, align: 'left' }, + { key: 'domainName', label: 'Domain', header: 'Domain', sortable: true, align: 'left', headerAlign: 'left' }, + { key: 'organizationName', label: 'Organization', header: 'Organization', sortable: true, align: 'left', headerAlign: 'left' }, { key: 'domainType', label: 'Type', header: 'Type', sortable: true, }, { key: 'isPrimary', label: 'Primary', header: 'Primary', sortable: true }, { key: 'sslStatus', label: 'SSL', header: 'SSL', sortable: true }, @@ -98,6 +98,7 @@ export class TenantDomainList implements OnInit { ngOnInit(): void { this.tableStore.initialize({ + defaultSort: { column: 'domainName' }, fetcher: query => this.tenantDomainApi.getTenantDomainDataTable( query, this.appliedTenantId(), diff --git a/src/app/features/settings/db-connections/pages/db-connection-list/db-connection-list.html b/src/app/features/settings/db-connections/pages/db-connection-list/db-connection-list.html index a9d47f13..806ab242 100644 --- a/src/app/features/settings/db-connections/pages/db-connection-list/db-connection-list.html +++ b/src/app/features/settings/db-connections/pages/db-connection-list/db-connection-list.html @@ -5,6 +5,8 @@ [totalRecords]="tableStore.totalRecords()" [pageIndex]="tableStore.queryState.pageIndex()" [pageSize]="tableStore.queryState.pageSize()" + [initialSortColumn]="tableStore.queryState.sortColumn()" + [initialSortDirection]="tableStore.queryState.sortDirection()" tableTitle="Database Connections" buttonTitle="Add" [showSearch]="true" diff --git a/src/app/features/settings/db-connections/pages/db-connection-list/db-connection-list.ts b/src/app/features/settings/db-connections/pages/db-connection-list/db-connection-list.ts index 1334ccd8..b3bcae90 100644 --- a/src/app/features/settings/db-connections/pages/db-connection-list/db-connection-list.ts +++ b/src/app/features/settings/db-connections/pages/db-connection-list/db-connection-list.ts @@ -41,13 +41,13 @@ export class DbConnectionList implements OnInit { key: 'connectionCode', label: 'Code', header: 'Code', sortable: true, headerAlign: 'center', align: 'center', width: '110px', badge: true, badgeClass: () => 'badge bg-primary/10 text-primary font-mono' }, - { key: 'connectionName', label: 'Connection Name', header: 'Connection Name', sortable: true, headerAlign: 'center', align: 'left' }, + { key: 'connectionName', label: 'Connection Name', header: 'Connection Name', sortable: true, headerAlign: 'left', align: 'left' }, { key: 'dbEngine', label: 'DB Engine', header: 'DB Engine', sortable: true, headerAlign: 'center', align: 'center', width: '120px', formatter: value => typeof value === 'string' ? value.toUpperCase() : '—' }, { - key: 'hostPort', label: 'Host / Port', header: 'Host / Port', sortable: true, headerAlign: 'center', align: 'left', + key: 'hostPort', label: 'Host / Port', header: 'Host / Port', sortable: true, headerAlign: 'left', align: 'left', formatter: (val, row) => { if (row.host && row.port) return `${row.host} : ${row.port}`; if (row.hostPort) { @@ -58,7 +58,7 @@ export class DbConnectionList implements OnInit { return typeof val === 'string' ? val : '—'; } }, - { key: 'databaseName', label: 'Database', header: 'Database', sortable: true, headerAlign: 'center', align: 'left' }, + { key: 'databaseName', label: 'Database', header: 'Database', sortable: true, headerAlign: 'left', align: 'left' }, { key: 'region', label: 'Region', header: 'Region', sortable: true, headerAlign: 'center', align: 'center', width: '90px', badge: true, badgeClass: () => 'badge bg-secondary/10 text-secondary' @@ -93,6 +93,7 @@ export class DbConnectionList implements OnInit { ngOnInit(): void { this.tableStore.initialize({ + defaultSort: { column: 'connectionCode' }, fetcher: query => this.dbConnectionApi.getDbConnectionDataTable(query), mapRow: (connection, serialNumber) => { const hostDisplay = connection.hostPort || (connection.host ? `${connection.host}:${connection.port || 5432}` : ''); diff --git a/src/app/features/tenants/pages/tenant-currencies/tenant-currencies.html b/src/app/features/tenants/pages/tenant-currencies/tenant-currencies.html index 73d4a18c..11fc2c57 100644 --- a/src/app/features/tenants/pages/tenant-currencies/tenant-currencies.html +++ b/src/app/features/tenants/pages/tenant-currencies/tenant-currencies.html @@ -36,7 +36,6 @@ [totalRecords]="totalRecords()" [pageIndex]="queryState.pageIndex()" [pageSize]="queryState.pageSize()" - [pageSizeOptions]="[5, 10, 20, 50]" tableTitle="Tenant Management" buttonTitle="Add Tenant" [showSearch]="true" 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 a64b2dec..cd8effa9 100644 --- a/src/app/features/tenants/pages/tenant-list/tenant-list.html +++ b/src/app/features/tenants/pages/tenant-list/tenant-list.html @@ -5,6 +5,8 @@ [totalRecords]="tableStore.filteredRecords()" [pageIndex]="tableStore.queryState.pageIndex()" [pageSize]="tableStore.queryState.pageSize()" + [initialSortColumn]="tableStore.queryState.sortColumn()" + [initialSortDirection]="tableStore.queryState.sortDirection()" tableTitle="Tenants" buttonTitle="Add" [showSearch]="true" diff --git a/src/app/features/tenants/pages/tenant-list/tenant-list.ts b/src/app/features/tenants/pages/tenant-list/tenant-list.ts index af7cdc4f..fab78516 100644 --- a/src/app/features/tenants/pages/tenant-list/tenant-list.ts +++ b/src/app/features/tenants/pages/tenant-list/tenant-list.ts @@ -61,6 +61,7 @@ export class TenantList implements OnInit { ngOnInit(): void { this.tableStore.initialize({ + defaultSort: { column: 'code' }, fetcher: query => this.tenantApi.getTenantDataTable(query), mapRow: (tenant, serialNumber) => ({ id: tenant.id, diff --git a/src/app/features/users/pages/users-list/users-list.html b/src/app/features/users/pages/users-list/users-list.html index 64bd877d..ee10ef52 100644 --- a/src/app/features/users/pages/users-list/users-list.html +++ b/src/app/features/users/pages/users-list/users-list.html @@ -5,7 +5,8 @@ (addClicked)="onAddUser()" [totalRecords]="tableStore.totalRecords()" [pageIndex]="tableStore.queryState.pageIndex()" - [pageSizeOptions]="[5, 10, 20, 50]" + [initialSortColumn]="tableStore.queryState.sortColumn()" + [initialSortDirection]="tableStore.queryState.sortDirection()" tableTitle="Users" buttonTitle="Add" [showSearch]="true" diff --git a/src/app/features/users/pages/users-list/users-list.ts b/src/app/features/users/pages/users-list/users-list.ts index d1618745..f2a56db4 100644 --- a/src/app/features/users/pages/users-list/users-list.ts +++ b/src/app/features/users/pages/users-list/users-list.ts @@ -114,6 +114,7 @@ export class UsersList implements OnInit { ngOnInit(): void { this.tableStore.initialize({ + defaultSort: { column: 'email' }, fetcher: query => this.usersApi.getDataTable(query), mapRow: (user, serialNumber) => ({ id: user.id, diff --git a/src/app/shared/components/data-list/data-list.html b/src/app/shared/components/data-list/data-list.html deleted file mode 100644 index e4185bb1..00000000 --- a/src/app/shared/components/data-list/data-list.html +++ /dev/null @@ -1,66 +0,0 @@ -
-
-
-
-

{{ title() }}

- @if (subtitle()) {

{{ subtitle() }}

} -
-
- - @if (showFilters()) { - - } -
- - - -
- - -
-
- @if (filtersOpen()) { -
- -
- - -
-
- } -
- -
- @if (loading()) { -
Loading data...
- } @else if (totalRecords() === 0) { -

{{ emptyMessage() }}

{{ emptyDescription() }}

- } @else { -
-
-
- } - -
-
diff --git a/src/app/shared/components/data-list/data-list.ts b/src/app/shared/components/data-list/data-list.ts deleted file mode 100644 index 5a9f57d3..00000000 --- a/src/app/shared/components/data-list/data-list.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Component, DestroyRef, computed, inject, input, output, signal } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { Subject, debounceTime, distinctUntilChanged } from 'rxjs'; - -export type DataListView = 'table' | 'tile' | 'detail'; -export type DataListDensity = 'compact' | 'comfortable' | 'spacious'; - -@Component({ - selector: 'app-data-list', - standalone: true, - templateUrl: './data-list.html' -}) -export class DataList { - private readonly destroyRef = inject(DestroyRef); - private readonly searchTerms$ = new Subject(); - - readonly title = input.required(); - readonly subtitle = input(''); - readonly addLabel = input('Add'); - readonly searchPlaceholder = input('Search...'); - readonly totalRecords = input(0); - readonly pageIndex = input(1); - readonly pageSize = input(10); - readonly pageSizeOptions = input([5, 10, 20, 50]); - readonly loading = input(false); - readonly emptyMessage = input('No records found.'); - readonly emptyDescription = input('Try changing your search or filters.'); - readonly showFilters = input(true); - - readonly searchChanged = output(); - readonly addClicked = output(); - readonly pageChanged = output<{ pageIndex: number; pageSize: number }>(); - readonly filtersCleared = output(); - readonly filtersApplied = output(); - - readonly view = signal('table'); - readonly density = signal('comfortable'); - readonly filtersOpen = signal(false); - readonly totalPages = computed(() => Math.max(1, Math.ceil(this.totalRecords() / this.pageSize()))); - readonly startRecord = computed(() => this.totalRecords() ? (this.pageIndex() - 1) * this.pageSize() + 1 : 0); - readonly endRecord = computed(() => Math.min(this.pageIndex() * this.pageSize(), this.totalRecords())); - readonly visiblePages = computed(() => { - const total = this.totalPages(); - const current = this.pageIndex(); - const start = Math.max(1, Math.min(current - 2, total - 4)); - return Array.from({ length: Math.min(5, total) }, (_, index) => start + index); - }); - readonly contentSpacingClass = computed(() => ({ - compact: 'gap-3', comfortable: 'gap-4', spacious: 'gap-6' - })[this.density()]); - readonly cardPaddingClass = computed(() => ({ - compact: 'p-3', comfortable: 'p-4', spacious: 'p-6' - })[this.density()]); - - constructor() { - this.searchTerms$.pipe( - debounceTime(300), - distinctUntilChanged(), - takeUntilDestroyed(this.destroyRef) - ).subscribe(value => this.searchChanged.emit(value)); - } - - setView(view: DataListView): void { this.view.set(view); } - setDensity(event: Event): void { this.density.set((event.target as HTMLSelectElement).value as DataListDensity); } - onSearch(value: string): void { this.searchTerms$.next(value.trim()); } - goToPage(page: number): void { - if (page >= 1 && page <= this.totalPages() && page !== this.pageIndex()) { - this.pageChanged.emit({ pageIndex: page - 1, pageSize: this.pageSize() }); - } - } - onPageSizeChange(event: Event): void { - const pageSize = Number((event.target as HTMLSelectElement).value); - if (pageSize && pageSize !== this.pageSize()) this.pageChanged.emit({ pageIndex: 0, pageSize }); - } -} diff --git a/src/app/shared/components/data-table/data-table-query.state.ts b/src/app/shared/components/data-table/data-table-query.state.ts index b692c89e..9b37fb75 100644 --- a/src/app/shared/components/data-table/data-table-query.state.ts +++ b/src/app/shared/components/data-table/data-table-query.state.ts @@ -12,6 +12,17 @@ export class DataTableQueryState { sortColumn = signal(''); sortDirection = signal<'asc' | 'desc'>('asc'); private readonly draw = signal(1); + private defaultSortColumn = ''; + private defaultSortDirection: 'asc' | 'desc' = 'asc'; + + setDefaultSort(column: string, direction: 'asc' | 'desc' = 'asc'): void { + this.defaultSortColumn = column; + this.defaultSortDirection = direction; + if (!this.sortColumn()) { + this.sortColumn.set(column); + this.sortDirection.set(direction); + } + } getQuery(): DataTableQuery { return { @@ -52,8 +63,8 @@ export class DataTableQueryState { this.pageIndex.set(1); this.pageSize.set(15); this.searchText.set(''); - this.sortColumn.set(''); - this.sortDirection.set('asc'); + this.sortColumn.set(this.defaultSortColumn); + this.sortDirection.set(this.defaultSortDirection); this.draw.set(1); return this.getQuery(); } diff --git a/src/app/shared/components/data-table/data-table.html b/src/app/shared/components/data-table/data-table.html index 066c63b4..d1f10c76 100644 --- a/src/app/shared/components/data-table/data-table.html +++ b/src/app/shared/components/data-table/data-table.html @@ -226,16 +226,9 @@ Show - + entries @@ -259,15 +252,21 @@ @for (page of visiblePages(); track page) { + @if (page === 'ellipsis-start' || page === 'ellipsis-end') { + + } @else {
  • } + }
  • diff --git a/src/app/shared/components/data-table/data-table.scss b/src/app/shared/components/data-table/data-table.scss index ce8848e3..f09c13de 100644 --- a/src/app/shared/components/data-table/data-table.scss +++ b/src/app/shared/components/data-table/data-table.scss @@ -142,10 +142,12 @@ // background-color: var(--color-light); background-color: #e1d0ff; + box-shadow: inset 3px 0 0 0 #7c3deb; } :host-context(.dark) .data-table-modern .table tbody tr:hover { background-color: color-mix(in srgb, #fff 6%, transparent); + box-shadow: inset 3px 0 0 0 #7c3deb; } /* Inactive row action buttons border */ @@ -218,6 +220,15 @@ background-color: transparent; } +.data-table-modern .ti-pagination li.page-item--ellipsis { + border: none; +} + +.data-table-modern .ti-pagination li.page-item--ellipsis .page-link { + color: var(--color-textmuted, #8c9097); + cursor: default; +} + :host-context(.dark) .data-table-modern .ti-pagination li .page-link { background-color: transparent; color: color-mix(in srgb, var(--color-defaulttextcolor) 80%, #fff); @@ -234,6 +245,93 @@ background-color: var(--color-primary) !important; } +/* Page size select — outline-primary pill, sized to fit "100", matching the + "Showing X to Y of Z entries" number color/weight. PrimeNG renders its own + structural CSS at runtime after this stylesheet, so equal-specificity + rules need !important to win the cascade tie (::ng-deep is global here + since the overlay panel is appended to , outside this component). */ +::ng-deep { + .data-table-page-size { + width: 4rem !important; + min-width: 4rem !important; + height: 1.5rem !important; + box-sizing: border-box; + border: 1px solid var(--color-primary) !important; + border-radius: 9999px !important; + background: var(--color-white) !important; + transition: box-shadow 0.2s ease, border-color 0.2s ease; + + &:not(.p-disabled).p-focus { + border-color: var(--color-primary) !important; + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 15%, transparent) !important; + } + + .p-select-label { + display: flex !important; + align-items: center !important; + height: 100% !important; + padding: 0 0 0 0.6rem !important; + font-size: 0.75rem !important; + line-height: 1 !important; + font-weight: 700 !important; + color: var(--color-defaulttextcolor) !important; + } + + .p-select-dropdown { + width: 1.25rem !important; + height: 100% !important; + margin-inline-end: 0.35rem; + color: var(--color-primary) !important; + } + } + + .data-table-page-size-panel { + border: 1px solid var(--color-primary) !important; + border-radius: 12px !important; + background: var(--color-white) !important; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.12), 0 8px 10px -6px rgba(0, 0, 0, 0.08) !important; + overflow: hidden; + padding: 0.25rem !important; + + .p-select-list { + padding: 0 !important; + margin: 0 !important; + gap: 1px; + } + + .p-select-option { + padding: 0.4rem 0.75rem !important; + font-size: 0.875rem !important; + color: var(--color-defaulttextcolor) !important; + border-radius: 8px !important; + cursor: pointer; + transition: background-color 0.15s ease, color 0.15s ease; + + &:hover { + background: color-mix(in srgb, var(--color-primary) 10%, transparent) !important; + color: var(--color-primary) !important; + } + + &.p-select-option-selected { + background: var(--color-primary) !important; + color: var(--color-white) !important; + } + } + } +} + +:host-context(.dark) { + ::ng-deep { + .data-table-page-size { + background: var(--color-bodybg) !important; + } + + .data-table-page-size-panel { + background: var(--color-bodybg) !important; + } + } +} + /* Responsive scroll */ .table-responsive { width: 100%; 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 ce426230..2f947020 100644 --- a/src/app/shared/components/data-table/data-table.store.ts +++ b/src/app/shared/components/data-table/data-table.store.ts @@ -15,6 +15,8 @@ export interface DataTableStoreOptions { fetcher: (query: DataTableQuery) => Observable>; mapRow?: (item: TItem, serialNumber: number, query: DataTableQuery) => TRow; onError?: (error: unknown) => void; + /** Column sorted by default on first load (e.g. the first sortable column after Sr. No.). */ + defaultSort?: { column: string; direction?: 'asc' | 'desc' }; } @Injectable() @@ -34,6 +36,10 @@ export class DataTableStore(false); initialize(options: DataTableStoreOptions): void { + if (options.defaultSort) { + this.queryState.setDefaultSort(options.defaultSort.column, options.defaultSort.direction ?? 'asc'); + } + this.querySubject$ .pipe( debounceTime(50), diff --git a/src/app/shared/components/data-table/data-table.ts b/src/app/shared/components/data-table/data-table.ts index 9fc380be..50545c63 100644 --- a/src/app/shared/components/data-table/data-table.ts +++ b/src/app/shared/components/data-table/data-table.ts @@ -4,6 +4,8 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; import { Subject } from 'rxjs'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormsModule } from '@angular/forms'; +import { SelectModule } from 'primeng/select'; import { DataTableCellDirective } from '../../directives/data-table-cell.directive'; import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedPosition } from '@angular/cdk/overlay'; @@ -20,6 +22,8 @@ import { DataTableFilterActionsDirective } from '../../directives/data-table-fil MatPaginatorModule, CdkOverlayOrigin, CdkConnectedOverlay, + FormsModule, + SelectModule, Button], templateUrl: './data-table.html', styleUrl: './data-table.scss', @@ -121,7 +125,7 @@ export class DataTable { totalRecords = input(0); pageIndex = input(1); pageSize = input(15); - pageSizeOptions = input([10, 15, 20, 50]); + pageSizeOptions = input([10, 15, 20, 50, 100]); showPaginator = input(true); /*---------------------------*/ @@ -157,8 +161,24 @@ export class DataTable { importClicked = output(); exportClicked = output(); + /** Column/direction the table should show as sorted on load (e.g. the store's current query sort). */ + initialSortColumn = input(''); + initialSortDirection = input<'asc' | 'desc'>('asc'); + sortColumn = signal(''); sortDirection = signal<'asc' | 'desc'>('asc'); + private sortInitialized = false; + + private readonly syncInitialSortEffect = effect(() => { + const column = this.initialSortColumn(); + const direction = this.initialSortDirection(); + if (!this.sortInitialized && column) { + this.sortInitialized = true; + this.sortColumn.set(column); + this.sortDirection.set(direction); + } + }); + openActionRowId = signal(null); tableClass = input('table table-bordered whitespace-nowrap min-w-full'); @@ -204,11 +224,15 @@ export class DataTable { return Math.min(this.pageIndex() * this.pageSize(), this.totalRecords()); }); - visiblePages = computed(() => { + visiblePages = computed<(number | 'ellipsis-start' | 'ellipsis-end')[]>(() => { const currentPage = this.pageIndex(); const total = this.totalPages(); const maxVisiblePages = 5; + if (total <= maxVisiblePages) { + return Array.from({ length: total }, (_, index) => index + 1); + } + let startPage = Math.max( 1, currentPage - Math.floor(maxVisiblePages / 2) @@ -226,10 +250,22 @@ export class DataTable { ); } - return Array.from( + const pages: (number | 'ellipsis-start' | 'ellipsis-end')[] = Array.from( { length: endPage - startPage + 1 }, (_, index) => startPage + index ); + + if (startPage > 1) { + if (startPage > 2) pages.unshift('ellipsis-start'); + pages.unshift(1); + } + + if (endPage < total) { + if (endPage < total - 1) pages.push('ellipsis-end'); + pages.push(total); + } + + return pages; }); goToPage(page: number): void { if ( @@ -247,11 +283,7 @@ export class DataTable { }); } - onPageSizeChange(event: Event): void { - const pageSize = Number( - (event.target as HTMLSelectElement).value - ); - + onPageSizeChange(pageSize: number): void { if (!pageSize || pageSize === this.pageSize()) { return; } diff --git a/src/app/shared/components/form/form-checkbox/form-checkbox.html b/src/app/shared/components/form/form-checkbox/form-checkbox.html deleted file mode 100644 index 6c7c593d..00000000 --- a/src/app/shared/components/form/form-checkbox/form-checkbox.html +++ /dev/null @@ -1 +0,0 @@ -

    form-checkbox works!

    diff --git a/src/app/shared/components/form/form-checkbox/form-checkbox.scss b/src/app/shared/components/form/form-checkbox/form-checkbox.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/src/app/shared/components/form/form-checkbox/form-checkbox.ts b/src/app/shared/components/form/form-checkbox/form-checkbox.ts deleted file mode 100644 index 0ce45d05..00000000 --- a/src/app/shared/components/form/form-checkbox/form-checkbox.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'form-checkbox', - imports: [], - templateUrl: './form-checkbox.html', - styleUrl: './form-checkbox.scss', -}) -export class FormCheckbox { - -} diff --git a/src/app/shared/components/form/form-file-upload/form-file-upload.html b/src/app/shared/components/form/form-file-upload/form-file-upload.html deleted file mode 100644 index 97a1488c..00000000 --- a/src/app/shared/components/form/form-file-upload/form-file-upload.html +++ /dev/null @@ -1 +0,0 @@ -

    form-file-upload works!

    diff --git a/src/app/shared/components/form/form-file-upload/form-file-upload.scss b/src/app/shared/components/form/form-file-upload/form-file-upload.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/src/app/shared/components/form/form-file-upload/form-file-upload.ts b/src/app/shared/components/form/form-file-upload/form-file-upload.ts deleted file mode 100644 index 126952fb..00000000 --- a/src/app/shared/components/form/form-file-upload/form-file-upload.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'form-file-upload', - imports: [], - templateUrl: './form-file-upload.html', - styleUrl: './form-file-upload.scss', -}) -export class FormFileUpload { - -} diff --git a/src/app/shared/components/form/form-radio/form-radio.html b/src/app/shared/components/form/form-radio/form-radio.html deleted file mode 100644 index 72cf9976..00000000 --- a/src/app/shared/components/form/form-radio/form-radio.html +++ /dev/null @@ -1 +0,0 @@ -

    form-radio works!

    diff --git a/src/app/shared/components/form/form-radio/form-radio.scss b/src/app/shared/components/form/form-radio/form-radio.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/src/app/shared/components/form/form-radio/form-radio.ts b/src/app/shared/components/form/form-radio/form-radio.ts deleted file mode 100644 index 013166e5..00000000 --- a/src/app/shared/components/form/form-radio/form-radio.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'form-radio', - imports: [], - templateUrl: './form-radio.html', - styleUrl: './form-radio.scss', -}) -export class FormRadio { - -} diff --git a/src/app/shared/components/form/form-select/form-select.ts b/src/app/shared/components/form/form-select/form-select.ts index 8fe9e9dc..a2b44d66 100644 --- a/src/app/shared/components/form/form-select/form-select.ts +++ b/src/app/shared/components/form/form-select/form-select.ts @@ -273,10 +273,19 @@ export class FormSelect implements ); }); + private readonly optionLabelCache = new Map>(); + readonly selectItems = computed[]>(() => { - const selected = new Set(this.activeSelectedValues()); - const selectedOptions = this.options() - .filter(option => selected.has(option.value)); + const options = this.options(); + + for (const option of options) { + this.optionLabelCache.set(option.value, option); + } + + const optionsByValue = new Map(options.map(option => [option.value, option])); + const selectedOptions = this.activeSelectedValues() + .map(value => optionsByValue.get(value) ?? this.optionLabelCache.get(value)) + .filter((option): option is FormSelectOption => option !== undefined); const optionValues = new Set(selectedOptions.map(option => option.value)); const visibleOptions = this.resolvedOptions() .filter(option => !optionValues.has(option.value)); diff --git a/src/app/shared/components/form/form-textarea/form-textarea.html b/src/app/shared/components/form/form-textarea/form-textarea.html deleted file mode 100644 index 1c38d096..00000000 --- a/src/app/shared/components/form/form-textarea/form-textarea.html +++ /dev/null @@ -1 +0,0 @@ -

    form-textarea works!

    diff --git a/src/app/shared/components/form/form-textarea/form-textarea.scss b/src/app/shared/components/form/form-textarea/form-textarea.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/src/app/shared/components/form/form-textarea/form-textarea.ts b/src/app/shared/components/form/form-textarea/form-textarea.ts deleted file mode 100644 index 4b87d7cf..00000000 --- a/src/app/shared/components/form/form-textarea/form-textarea.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'form-textarea', - imports: [], - templateUrl: './form-textarea.html', - styleUrl: './form-textarea.scss', -}) -export class FormTextarea { - -} diff --git a/src/app/shell/ui/sidebar/sidebar.html b/src/app/shell/ui/sidebar/sidebar.html index ec13545c..6c46e93d 100644 --- a/src/app/shell/ui/sidebar/sidebar.html +++ b/src/app/shell/ui/sidebar/sidebar.html @@ -100,6 +100,9 @@ + @if (childrenItem.icon) { + + } {{childrenItem.title}} @if(childrenItem.badgeClass){ @@ -111,6 +114,9 @@ @if (childrenItem.type === 'empty' ) { + @if (childrenItem.icon) { + + } {{childrenItem.title }} } @@ -119,6 +125,9 @@ + @if (childrenItem.icon) { + + } {{childrenItem.title}} diff --git a/src/app/shell/ui/sidebar/sidebar.scss b/src/app/shell/ui/sidebar/sidebar.scss index e69de29b..475ea8dd 100644 --- a/src/app/shell/ui/sidebar/sidebar.scss +++ b/src/app/shell/ui/sidebar/sidebar.scss @@ -0,0 +1,10 @@ +/* Sub-menu items now render a leading icon instead of a bullet dot. */ +.side-menu__item:before { + display: none !important; +} + +/* Tighten sub-menu left indent now that the bullet dot is gone. */ +.slide-menu.child1 .side-menu__item, +.slide-menu.child2 .side-menu__item { + padding-inline-start: 1.1rem !important; +}