From db49e7cb914f8b18a1b87e45075749ac4636f5d4 Mon Sep 17 00:00:00 2001 From: Gagan7900 <54118395+Gagan7900@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:48:05 +0530 Subject: [PATCH] multi-selection filter enable in all screen, icon added in sidebr sub-menus, remove un-used components, add border on left of row on hover, on click same open menu page reload --- public/assets/fonts/EtihadAltis-Text_V3.ttf | Bin 0 -> 129364 bytes src/app/app.config.ts | 4 +- src/app/core/services/common/menu.data.ts | 40 +++--- .../strategies/reload-route-reuse.strategy.ts | 9 ++ .../plans-subscriptions.html | 2 + .../plans-subscriptions.ts | 15 ++- .../cities/data-access/city.service.ts | 12 +- .../cities/pages/city-list/city-list.html | 40 +++--- .../cities/pages/city-list/city-list.ts | 126 ++++++++++-------- .../countries/data-access/country.service.ts | 7 +- .../pages/country-list/country-list.html | 13 +- .../pages/country-list/country-list.ts | 71 +++++----- .../pages/currency-list/currency-list.html | 2 + .../pages/currency-list/currency-list.ts | 16 ++- .../exchange-rate-list.html | 10 +- .../exchange-rate-list/exchange-rate-list.ts | 19 +-- .../pages/language-list/language-list.html | 2 + .../pages/language-list/language-list.ts | 13 +- .../plans/pages/plan-list/plan-list.html | 2 + .../plans/pages/plan-list/plan-list.ts | 21 +-- .../pages/region-list/region-list.html | 2 + .../regions/pages/region-list/region-list.ts | 11 +- .../states/data-access/state.service.ts | 21 +-- .../states/pages/state-list/state-list.html | 13 +- .../states/pages/state-list/state-list.ts | 69 +++++----- .../pages/timezone-list/timezone-list.html | 4 +- .../pages/timezone-list/timezone-list.ts | 11 +- .../translation-key-list.html | 2 + .../translation-key-list.ts | 12 +- .../translation-list/translation-list.html | 2 + .../translation-list/translation-list.ts | 9 +- .../organizations/dashboard/dashboard.ts | 1 + .../organization-awaiting-db.html | 2 + .../organization-awaiting-db.ts | 3 + .../organization-list/organization-list.html | 2 + .../organization-list/organization-list.ts | 2 + .../tenant-domain-table.html | 2 + .../tenant-domain-table.ts | 2 + .../tenant-domain-list.html | 2 + .../tenant-domain-list/tenant-domain-list.ts | 5 +- .../db-connection-list.html | 2 + .../db-connection-list/db-connection-list.ts | 7 +- .../tenant-currencies/tenant-currencies.html | 1 - .../pages/tenant-list/tenant-list.html | 2 + .../tenants/pages/tenant-list/tenant-list.ts | 1 + .../users/pages/users-list/users-list.html | 3 +- .../users/pages/users-list/users-list.ts | 1 + .../components/data-list/data-list.html | 66 --------- .../shared/components/data-list/data-list.ts | 75 ----------- .../data-table/data-table-query.state.ts | 15 ++- .../components/data-table/data-table.html | 21 ++- .../components/data-table/data-table.scss | 98 ++++++++++++++ .../components/data-table/data-table.store.ts | 6 + .../components/data-table/data-table.ts | 48 +++++-- .../form/form-checkbox/form-checkbox.html | 1 - .../form/form-checkbox/form-checkbox.scss | 0 .../form/form-checkbox/form-checkbox.ts | 11 -- .../form-file-upload/form-file-upload.html | 1 - .../form-file-upload/form-file-upload.scss | 0 .../form/form-file-upload/form-file-upload.ts | 11 -- .../form/form-radio/form-radio.html | 1 - .../form/form-radio/form-radio.scss | 0 .../components/form/form-radio/form-radio.ts | 11 -- .../form/form-select/form-select.ts | 15 ++- .../form/form-textarea/form-textarea.html | 1 - .../form/form-textarea/form-textarea.scss | 0 .../form/form-textarea/form-textarea.ts | 11 -- src/app/shell/ui/sidebar/sidebar.html | 9 ++ src/app/shell/ui/sidebar/sidebar.scss | 10 ++ 69 files changed, 562 insertions(+), 467 deletions(-) create mode 100644 public/assets/fonts/EtihadAltis-Text_V3.ttf create mode 100644 src/app/core/strategies/reload-route-reuse.strategy.ts delete mode 100644 src/app/shared/components/data-list/data-list.html delete mode 100644 src/app/shared/components/data-list/data-list.ts delete mode 100644 src/app/shared/components/form/form-checkbox/form-checkbox.html delete mode 100644 src/app/shared/components/form/form-checkbox/form-checkbox.scss delete mode 100644 src/app/shared/components/form/form-checkbox/form-checkbox.ts delete mode 100644 src/app/shared/components/form/form-file-upload/form-file-upload.html delete mode 100644 src/app/shared/components/form/form-file-upload/form-file-upload.scss delete mode 100644 src/app/shared/components/form/form-file-upload/form-file-upload.ts delete mode 100644 src/app/shared/components/form/form-radio/form-radio.html delete mode 100644 src/app/shared/components/form/form-radio/form-radio.scss delete mode 100644 src/app/shared/components/form/form-radio/form-radio.ts delete mode 100644 src/app/shared/components/form/form-textarea/form-textarea.html delete mode 100644 src/app/shared/components/form/form-textarea/form-textarea.scss delete mode 100644 src/app/shared/components/form/form-textarea/form-textarea.ts diff --git a/public/assets/fonts/EtihadAltis-Text_V3.ttf b/public/assets/fonts/EtihadAltis-Text_V3.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ec5eedad21e6307dada48efeadc03d45354ced9d GIT binary patch literal 129364 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' }, ], }, //{ 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; +}