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
This commit is contained in:
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
|
||||
import { provideRouter, withRouterConfig } from '@angular/router';
|
||||
import { provideRouter, withRouterConfig, RouteReuseStrategy } from '@angular/router';
|
||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
import { App_Route } from './app.routes';
|
||||
@@ -8,10 +8,12 @@ import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
|
||||
import { authInterceptor } from './core/interceptors/auth.interceptor';
|
||||
import { errorInterceptor } from './core/interceptors/error.interceptor';
|
||||
import { loadingInterceptor } from './core/interceptors/loading-interceptor';
|
||||
import { ReloadRouteReuseStrategy } from './core/strategies/reload-route-reuse.strategy';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideRouter(App_Route, withRouterConfig({ onSameUrlNavigation: 'reload' })),
|
||||
{ provide: RouteReuseStrategy, useClass: ReloadRouteReuseStrategy },
|
||||
provideHttpClient(withInterceptors([authInterceptor, errorInterceptor,loadingInterceptor])),
|
||||
provideAnimations(),
|
||||
provideCharts(withDefaultRegisterables()),
|
||||
|
||||
@@ -12,7 +12,7 @@ export const SAAS_MENU_DATA: MenuContext = {
|
||||
selected: false,
|
||||
dirchange: false,
|
||||
children: [
|
||||
{ path: '/dashboards/crm', title: 'CRM', type: 'link', dirchange: false },
|
||||
{ path: '/dashboards/crm', title: 'CRM', type: 'link', dirchange: false, icon: '<i class="bx bx-line-chart side-menu__icon"></i>' },
|
||||
],
|
||||
},
|
||||
//{ 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: '<i class="bx bx-coin-stack side-menu__icon"></i>' },
|
||||
{ path: '/global-masters/exchange-rates', title: 'Exchange Rates', type: 'link', dirchange: false, icon: '<i class="bx bx-trending-up side-menu__icon"></i>' },
|
||||
{ path: '/global-masters/languages', title: 'Language', type: 'link', dirchange: false, icon: '<i class="bx bx-world side-menu__icon"></i>' },
|
||||
{ path: '/global-masters/timezones', title: 'Timezone', type: 'link', dirchange: false, icon: '<i class="bx bx-time-five side-menu__icon"></i>' },
|
||||
{ path: '/global-masters/regions', title: 'Region', type: 'link', dirchange: false, icon: '<i class="bx bx-map-alt side-menu__icon"></i>' },
|
||||
{ path: '/global-masters/countries', title: 'Country', type: 'link', dirchange: false, icon: '<i class="bx bx-flag side-menu__icon"></i>' },
|
||||
{ path: '/global-masters/states', title: 'State', type: 'link', dirchange: false, icon: '<i class="bx bx-map side-menu__icon"></i>' },
|
||||
{ path: '/global-masters/cities', title: 'City', type: 'link', dirchange: false, icon: '<i class="bx bx-building-house side-menu__icon"></i>' },
|
||||
{ path: '/global-masters/plans', title: 'Plans', type: 'link', dirchange: false, icon: '<i class="bx bx-package side-menu__icon"></i>' },
|
||||
|
||||
// {
|
||||
// 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: '<i class="bx bx-grid-alt side-menu__icon"></i>' },
|
||||
{ path: '/organizations/list', title: 'Organization List', type: 'link', dirchange: false, icon: '<i class="bx bx-list-ul side-menu__icon"></i>' },
|
||||
{ path: '/organizations/awaiting-database', title: 'Assign Database', type: 'link', dirchange: false, icon: '<i class="bx bx-data side-menu__icon"></i>' },
|
||||
{ path: '/organizations/tenant-domains', title: 'Tenant Domains', type: 'link', dirchange: false, icon: '<i class="bx bx-globe-alt side-menu__icon"></i>' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -69,8 +69,8 @@ export const SAAS_MENU_DATA: MenuContext = {
|
||||
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 },
|
||||
{ path: '/localization/translation-keys', title: 'Translations Keys', type: 'link', dirchange: false, icon: '<i class="bx bx-key side-menu__icon"></i>' },
|
||||
{ path: '/localization/translations-manager', title: 'Translations', type: 'link', dirchange: false, icon: '<i class="bx bx-book-content side-menu__icon"></i>' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -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: '<i class="bx bx-receipt side-menu__icon"></i>' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -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: '<i class="bx bx-palette side-menu__icon"></i>' },
|
||||
{ path: '/settings/db-connections', title: 'Database Connections', type: 'link', dirchange: false, icon: '<i class="bx bx-server side-menu__icon"></i>' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+2
@@ -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"
|
||||
|
||||
+8
-7
@@ -117,21 +117,21 @@ export class PlansSubscriptions implements OnInit {
|
||||
});
|
||||
|
||||
readonly columns = signal<DataTableColumn<SubscriptionTableRow>[]>([
|
||||
{ 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,
|
||||
|
||||
@@ -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<DataTableResult<CityDto>> {
|
||||
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<DataTableResult<CityDto>>(
|
||||
CITY_ENDPOINTS.dataTable,
|
||||
query,
|
||||
|
||||
@@ -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 @@
|
||||
<form [formGroup]="filterForm" (ngSubmit)="onApplyFilter($event)" autocomplete="off"
|
||||
class="flex flex-wrap items-end gap-3 w-full">
|
||||
<div class="w-64 min-w-[200px]">
|
||||
<app-autocomplete
|
||||
<app-form-select
|
||||
formControlName="countryId"
|
||||
inputId="city-country-filter"
|
||||
variant="floating"
|
||||
size="sm"
|
||||
label="Country"
|
||||
placeholder="Search country"
|
||||
[searchFn]="filterCountrySearchFn"
|
||||
[valueWith]="filterCountryValueFn"
|
||||
[displayWith]="filterCountryDisplayFn"
|
||||
[selectedItem]="selectedCountry()"
|
||||
[minSearchLength]="0"
|
||||
[debounceTime]="300"
|
||||
[limit]="50"
|
||||
placeholder="All Countries"
|
||||
mode="multiple"
|
||||
searchMode="server"
|
||||
[searchable]="true"
|
||||
[options]="countryOptions()"
|
||||
[loading]="countryOptionsLoading()"
|
||||
[clearable]="true"
|
||||
[hideValidation]="true"
|
||||
appendTo="body"
|
||||
wrapperClass="!mb-0 w-full"
|
||||
(itemSelected)="onFilterCountryChanged($event)"
|
||||
(searchChanged)="onCountrySearchChanged($event.term)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-64 min-w-[200px]">
|
||||
<app-autocomplete
|
||||
<app-form-select
|
||||
formControlName="stateId"
|
||||
inputId="city-state-filter"
|
||||
variant="floating"
|
||||
size="sm"
|
||||
label="State"
|
||||
placeholder="Search state"
|
||||
[searchFn]="filterStateSearchFn"
|
||||
[valueWith]="filterStateValueFn"
|
||||
[displayWith]="filterStateDisplayFn"
|
||||
[selectedItem]="selectedFilterState()"
|
||||
[minSearchLength]="0"
|
||||
[debounceTime]="300"
|
||||
[limit]="50"
|
||||
placeholder="All States"
|
||||
mode="multiple"
|
||||
searchMode="server"
|
||||
[searchable]="true"
|
||||
[options]="stateOptions()"
|
||||
[loading]="stateOptionsLoading()"
|
||||
[clearable]="true"
|
||||
[hideValidation]="true"
|
||||
appendTo="body"
|
||||
wrapperClass="!mb-0 w-full"
|
||||
(itemSelected)="onFilterStateChanged($event)"
|
||||
(searchChanged)="onStateSearchChanged($event.term)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
@@ -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<CityDto, CityTableRow>);
|
||||
|
||||
readonly selectedCountry = signal<CountryLookupDto | null>(null);
|
||||
readonly selectedFilterState = signal<StateLookupDto | null>(null);
|
||||
readonly statusChangingId = signal<string | null>(null);
|
||||
readonly deletingId = signal<string | null>(null);
|
||||
readonly pendingDeleteCity = signal<CityTableRow | null>(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<FormSelectOption<string>[]>([]);
|
||||
readonly countryOptionsLoading = signal(false);
|
||||
readonly stateOptions = signal<FormSelectOption<string>[]>([]);
|
||||
readonly stateOptionsLoading = signal(false);
|
||||
private readonly countrySearch$ = new Subject<string>();
|
||||
private readonly stateSearch$ = new Subject<string>();
|
||||
|
||||
readonly columns = signal<DataTableColumn<CityTableRow>[]>([
|
||||
{ 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<CountryLookupDto> = (term, page) =>
|
||||
this.countryApi.autocomplete(term, page);
|
||||
readonly filterCountryValueFn: AutocompleteValueFn<CountryLookupDto, string> = c => c.id;
|
||||
readonly filterCountryDisplayFn: AutocompleteDisplayFn<CountryLookupDto> = c => c.name;
|
||||
|
||||
readonly filterStateSearchFn: AutocompleteSearchFn<StateLookupDto> = (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<StateLookupDto, string> = s => s.id;
|
||||
readonly filterStateDisplayFn: AutocompleteDisplayFn<StateLookupDto> = 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<DataTableResult<CountryDto>> {
|
||||
const params = regionId ? new HttpParams().set('regionId', regionId) : undefined;
|
||||
getCountryDataTable(query: DataTableQuery, regionIds: readonly string[] = []): Observable<DataTableResult<CountryDto>> {
|
||||
let params = new HttpParams();
|
||||
for (const regionId of regionIds) {
|
||||
params = params.append('regionIds', regionId);
|
||||
}
|
||||
return this.http.post<DataTableResult<CountryDto>>(COUNTRY_ENDPOINTS.dataTable, query, { params });
|
||||
}
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
<form [formGroup]="regionFilterForm" (ngSubmit)="onApplyFilter()" autocomplete="off"
|
||||
class="flex flex-wrap items-end gap-3 w-full">
|
||||
<div class="w-64 min-w-[200px]">
|
||||
<app-autocomplete formControlName="regionId" inputId="country-region-filter" variant="floating" size="sm"
|
||||
label="Region" placeholder="Search region" [searchFn]="searchRegions" [displayWith]="displayRegion"
|
||||
[valueWith]="regionValue" [selectedItem]="selectedRegionLookup()" [minSearchLength]="1" [debounceTime]="300"
|
||||
[limit]="50" [clearable]="true" [hideValidation]="true" wrapperClass="!mb-0 w-full"
|
||||
(itemSelected)="onFilterRegionSelected($event)" />
|
||||
<app-form-select formControlName="regionId" inputId="country-region-filter" variant="floating" size="sm"
|
||||
label="Region" placeholder="All Regions" mode="multiple" searchMode="server" [searchable]="true"
|
||||
[options]="regionOptions()" [loading]="regionOptionsLoading()" [clearable]="true" [hideValidation]="true"
|
||||
appendTo="body" wrapperClass="!mb-0 w-full" (searchChanged)="onRegionSearchChanged($event.term)" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<app-button action="custom" label="Apply" icon="ti ti-filter" variant="primary-full" type="submit" size="sm"
|
||||
@@ -49,7 +50,7 @@
|
||||
(error)="onFlagError($event)"
|
||||
/>
|
||||
}
|
||||
<span class="font-semibold">{{ value }}</span>
|
||||
<span>{{ value }}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-data-table>
|
||||
|
||||
@@ -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<RegionLookupDto | null>(null);
|
||||
|
||||
readonly regionFilterForm = this.formBuilder.nonNullable.group({
|
||||
regionId: ['']
|
||||
regionId: [[] as string[]]
|
||||
});
|
||||
|
||||
readonly searchRegions: AutocompleteSearchFn<RegionLookupDto> = (term, limit) =>
|
||||
this.regionApi.autocomplete(term, limit);
|
||||
readonly displayRegion: AutocompleteDisplayFn<RegionLookupDto> = region => region.name;
|
||||
readonly regionValue: AutocompleteValueFn<RegionLookupDto, string> = region => region.id;
|
||||
readonly resolveRegion: AutocompleteResolveValueFn<RegionLookupDto, string> = value =>
|
||||
this.regionApi.getRegionById(value).pipe(
|
||||
map(region => ({ id: region.id, code: region.code, name: region.name }))
|
||||
);
|
||||
readonly regionOptions = signal<FormSelectOption<string>[]>([]);
|
||||
readonly regionOptionsLoading = signal(false);
|
||||
private readonly regionSearch$ = new Subject<string>();
|
||||
|
||||
readonly columns = signal<DataTableColumn<CountryTableRow>[]>([
|
||||
{ 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -84,14 +84,15 @@ export class CurrencyList implements OnInit {
|
||||
private iso2TooltipCloseTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
readonly columns = signal<DataTableColumn<CurrencyTableRow>[]>([
|
||||
{ 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 {
|
||||
|
||||
+6
-4
@@ -1,6 +1,8 @@
|
||||
<app-data-table [columns]="columns()" [rows]="tableStore.rows()" [actions]="actions()"
|
||||
[totalRecords]="tableStore.filteredRecords()" [pageIndex]="tableStore.queryState.pageIndex()"
|
||||
[pageSize]="tableStore.queryState.pageSize()" tableTitle="Exchange Rates (ROE)" buttonTitle="Add"
|
||||
[pageSize]="tableStore.queryState.pageSize()"
|
||||
[initialSortColumn]="tableStore.queryState.sortColumn()" [initialSortDirection]="tableStore.queryState.sortDirection()"
|
||||
tableTitle="Exchange Rates (ROE)" buttonTitle="Add"
|
||||
[showSearch]="true" [showAddButton]="true" [showFilterButton]="true" [filterActive]="showFilters()"
|
||||
searchPlaceholder="Search pair..." [searchDebounceTime]="300" toolTip="Add New Rate" (addClicked)="onAddRate()"
|
||||
(searchChanged)="tableStore.onSearch($event)" (pageChanged)="tableStore.onPageChange($event)"
|
||||
@@ -26,21 +28,21 @@
|
||||
|
||||
<!-- Custom From Currency Cell -->
|
||||
<ng-template appDataTableCell="fromCurrencyCode" let-row let-value="value">
|
||||
<span class="font-bold text-gray-800 dark:text-gray-200 tracking-wide">
|
||||
<span class="text-gray-800 dark:text-gray-200 tracking-wide">
|
||||
{{ getCurrencyDisplay(row.fromCurrencyId, value) }}
|
||||
</span>
|
||||
</ng-template>
|
||||
|
||||
<!-- Custom To Currency Cell -->
|
||||
<ng-template appDataTableCell="toCurrencyCode" let-row let-value="value">
|
||||
<span class="font-bold text-gray-800 dark:text-gray-200 tracking-wide">
|
||||
<span class="text-gray-800 dark:text-gray-200 tracking-wide">
|
||||
{{ getCurrencyDisplay(row.toCurrencyId, value) }}
|
||||
</span>
|
||||
</ng-template>
|
||||
|
||||
<!-- Custom Rate Cell -->
|
||||
<ng-template appDataTableCell="rate" let-row let-value="value">
|
||||
<span class="font-mono font-semibold text-primary dark:text-primary-light">
|
||||
<span class="font-mono text-primary dark:text-primary-light">
|
||||
{{ value | number:'1.4-4' }}
|
||||
</span>
|
||||
</ng-template>
|
||||
|
||||
+10
-9
@@ -87,15 +87,15 @@ export class ExchangeRateList implements OnInit {
|
||||
});
|
||||
|
||||
readonly columns = signal<DataTableColumn<ExchangeRateTableRow>[]>([
|
||||
{ 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<DataTableAction<ExchangeRateTableRow>[]>([
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -48,17 +48,17 @@ export class LanguageList implements OnInit {
|
||||
readonly deleteConfirmDialog = viewChild(ConfirmDialog);
|
||||
|
||||
readonly columns = signal<DataTableColumn<LanguageTableRow>[]>([
|
||||
{ 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)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -41,23 +41,23 @@ export class PlanList implements OnInit {
|
||||
readonly deleteConfirmDialog = viewChild(ConfirmDialog);
|
||||
|
||||
readonly columns = signal<DataTableColumn<PlanTableRow>[]>([
|
||||
{ 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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -47,17 +47,17 @@ export class RegionList implements OnInit {
|
||||
readonly deleteConfirmDialog = viewChild(ConfirmDialog);
|
||||
|
||||
readonly columns = signal<DataTableColumn<RegionTableRow>[]>([
|
||||
{ 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)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,8 +18,11 @@ export class StateService {
|
||||
|
||||
private readonly http = inject(HttpClient);
|
||||
|
||||
getStateDataTable(query: DataTableQuery, countryId: string | null = null): Observable<DataTableResult<StateDto>> {
|
||||
const params = countryId ? new HttpParams().set('countryId', countryId) : undefined;
|
||||
getStateDataTable(query: DataTableQuery, countryIds: readonly string[] = []): Observable<DataTableResult<StateDto>> {
|
||||
let params = new HttpParams();
|
||||
for (const countryId of countryIds) {
|
||||
params = params.append('countryIds', countryId);
|
||||
}
|
||||
return this.http.post<DataTableResult<StateDto>>(`${STATE_ENDPOINTS.dataTable}`, query, { params });
|
||||
}
|
||||
|
||||
@@ -43,12 +46,12 @@ export class StateService {
|
||||
return this.http.get<StateDto>(STATE_ENDPOINTS.getById(id));
|
||||
}
|
||||
|
||||
autocomplete(countryId: string, term = '', limit = 50): Observable<StateLookupDto[]> {
|
||||
return this.http.get<StateLookupDto[]>(STATE_ENDPOINTS.autocomplete, {
|
||||
params: new HttpParams()
|
||||
.set('countryId', countryId)
|
||||
.set('term', term)
|
||||
.set('limit', limit)
|
||||
});
|
||||
autocomplete(countryIds: string | readonly string[], term = '', limit = 50): Observable<StateLookupDto[]> {
|
||||
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<StateLookupDto[]>(STATE_ENDPOINTS.autocomplete, { params });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<app-data-table [columns]="columns()" [rows]="tableStore.rows()" [actions]="actions()"
|
||||
[totalRecords]="tableStore.totalRecords()" [pageIndex]="tableStore.queryState.pageIndex()"
|
||||
[pageSize]="tableStore.queryState.pageSize()" tableTitle="States" buttonTitle="Add" [showSearch]="true"
|
||||
[pageSize]="tableStore.queryState.pageSize()"
|
||||
[initialSortColumn]="tableStore.queryState.sortColumn()" [initialSortDirection]="tableStore.queryState.sortDirection()"
|
||||
tableTitle="States" buttonTitle="Add" [showSearch]="true"
|
||||
[showAddButton]="true" [showFilterButton]="true" [filterActive]="showFilters()" searchPlaceholder="Search states..."
|
||||
[searchDebounceTime]="300" toolTip="Add State" (addClicked)="onAddState()"
|
||||
(searchChanged)="tableStore.onSearch($event)" (pageChanged)="tableStore.onPageChange($event)"
|
||||
@@ -10,11 +12,10 @@
|
||||
<form [formGroup]="countryFilterForm" (ngSubmit)="onApplyFilter()" autocomplete="off"
|
||||
class="flex flex-wrap items-end gap-3 w-full">
|
||||
<div class="w-64 min-w-[200px]">
|
||||
<app-autocomplete formControlName="countryId" inputId="state-country-filter" variant="floating" size="sm"
|
||||
label="Country" placeholder="Search country" [searchFn]="searchCountries" [displayWith]="displayCountry"
|
||||
[valueWith]="countryValue" [selectedItem]="selectedCountryLookup()" [minSearchLength]="1" [debounceTime]="300"
|
||||
[limit]="50" [clearable]="true" [hideValidation]="true" wrapperClass="!mb-0 w-full"
|
||||
(itemSelected)="onFilterCountrySelected($event)" />
|
||||
<app-form-select formControlName="countryId" inputId="state-country-filter" variant="floating" size="sm"
|
||||
label="Country" placeholder="All Countries" mode="multiple" searchMode="server" [searchable]="true"
|
||||
[options]="countryOptions()" [loading]="countryOptionsLoading()" [clearable]="true" [hideValidation]="true"
|
||||
appendTo="body" wrapperClass="!mb-0 w-full" (searchChanged)="onCountrySearchChanged($event.term)" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<app-button action="custom" label="Apply" icon="ti ti-filter" variant="primary-full" type="submit" size="sm"
|
||||
|
||||
@@ -1,11 +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, 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 { CountryLookupDto, CountryService } from '../../../countries/public-api';
|
||||
import { CountryService } from '../../../countries/public-api';
|
||||
import { StateDto, UpdateStateRequest } from '../../models/state.model';
|
||||
import { StateService } from '../../data-access/state.service';
|
||||
import { DataTable, DataTableToolbarDirective } from '../../../../../shared/components/data-table/data-table';
|
||||
@@ -16,13 +16,8 @@ import {
|
||||
DataTableColumn,
|
||||
DataTableRecord
|
||||
} from '../../../../../shared/components/data-table/data-table.types';
|
||||
import { Autocomplete } from '../../../../../shared/components/form/autocomplete/autocomplete';
|
||||
import {
|
||||
AutocompleteDisplayFn,
|
||||
AutocompleteResolveValueFn,
|
||||
AutocompleteSearchFn,
|
||||
AutocompleteValueFn
|
||||
} from '../../../../../shared/components/form/autocomplete/autocomplete.types';
|
||||
import { 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 { StateFormModalComponent } from '../../components/state-form-modal/state-form-modal';
|
||||
@@ -45,7 +40,7 @@ interface StateTableRow extends DataTableRecord {
|
||||
DataTable,
|
||||
DataTableToolbarDirective,
|
||||
ReactiveFormsModule,
|
||||
Autocomplete,
|
||||
FormSelect,
|
||||
ConfirmDialog,
|
||||
AppButton,
|
||||
StateFormModalComponent
|
||||
@@ -62,7 +57,6 @@ export class StateList implements OnInit {
|
||||
private readonly notification = inject(NotificationService);
|
||||
readonly tableStore = inject(DataTableStore<StateDto, StateTableRow>);
|
||||
|
||||
readonly selectedCountryLookup = signal<CountryLookupDto | null>(null);
|
||||
readonly statusChangingId = signal<string | null>(null);
|
||||
readonly deletingId = signal<string | null>(null);
|
||||
readonly pendingDeleteState = signal<StateTableRow | null>(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<CountryLookupDto> = (term, limit) =>
|
||||
this.countryApi.autocomplete(term, limit);
|
||||
readonly displayCountry: AutocompleteDisplayFn<CountryLookupDto> = country => country.name;
|
||||
readonly countryValue: AutocompleteValueFn<CountryLookupDto, string> = country => country.id;
|
||||
readonly resolveCountry: AutocompleteResolveValueFn<CountryLookupDto, string> = value =>
|
||||
this.countryApi.getCountryById(value).pipe(
|
||||
map(country => ({ id: country.id, iso2: country.iso2, name: country.name }))
|
||||
);
|
||||
readonly countryOptions = signal<FormSelectOption<string>[]>([]);
|
||||
readonly countryOptionsLoading = signal(false);
|
||||
private readonly countrySearch$ = new Subject<string>();
|
||||
|
||||
readonly columns = signal<DataTableColumn<StateTableRow>[]>([
|
||||
{ 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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)"
|
||||
>
|
||||
<ng-template appDataTableCell="ianaId" let-value="value">
|
||||
<span class="block max-w-72 truncate font-semibold" title="{{ value }}">{{ value }}</span>
|
||||
<span class="block max-w-72 truncate" title="{{ value }}">{{ value }}</span>
|
||||
</ng-template>
|
||||
<ng-template appDataTableCell="displayName" let-value="value">
|
||||
<span class="block max-w-72 truncate" title="{{ value }}">{{ value }}</span>
|
||||
|
||||
@@ -60,15 +60,15 @@ export class TimezoneList implements OnInit {
|
||||
readonly deleteConfirmDialog = viewChild(ConfirmDialog);
|
||||
|
||||
readonly columns = signal<DataTableColumn<TimezoneTableRow>[]>([
|
||||
{ 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)
|
||||
});
|
||||
}
|
||||
|
||||
+2
@@ -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"
|
||||
|
||||
+7
-5
@@ -58,8 +58,8 @@ export class TranslationKeyList implements OnInit {
|
||||
});
|
||||
|
||||
readonly columns = signal<DataTableColumn<TranslationKeyTableRow>[]>([
|
||||
{ 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<DataTableAction<TranslationKeyTableRow>[]>([
|
||||
@@ -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,
|
||||
|
||||
+2
@@ -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"
|
||||
|
||||
+6
-3
@@ -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<TranslationMatrixTableRow>[] = 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) => {
|
||||
|
||||
@@ -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 },
|
||||
{
|
||||
|
||||
+2
@@ -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"
|
||||
|
||||
+3
@@ -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 || '—';
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
+2
@@ -5,6 +5,8 @@
|
||||
[totalRecords]="totalRecords()"
|
||||
[pageIndex]="pageIndex()"
|
||||
[pageSize]="pageSize()"
|
||||
[initialSortColumn]="sortColumn()"
|
||||
[initialSortDirection]="sortDirection()"
|
||||
tableTitle="Tenant Domains"
|
||||
buttonTitle="Add"
|
||||
[showSearch]="true"
|
||||
|
||||
+2
@@ -34,6 +34,8 @@ export class TenantDomainTableComponent {
|
||||
readonly totalRecords = input<number>(0);
|
||||
readonly pageIndex = input<number>(1);
|
||||
readonly pageSize = input<number>(10);
|
||||
readonly sortColumn = input<string>('');
|
||||
readonly sortDirection = input<'asc' | 'desc'>('asc');
|
||||
readonly showFilters = input<boolean>(false);
|
||||
|
||||
readonly addClicked = output<void>();
|
||||
|
||||
@@ -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()"
|
||||
|
||||
@@ -56,8 +56,8 @@ export class TenantDomainList implements OnInit {
|
||||
|
||||
readonly columns = signal<DataTableColumn<TenantDomainTableRow>[]>([
|
||||
{ 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(),
|
||||
|
||||
+2
@@ -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"
|
||||
|
||||
+4
-3
@@ -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}` : '');
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<section class="space-y-4">
|
||||
<div class="box custom-box !mb-0">
|
||||
<div class="box-header !items-start !gap-4 flex-col xl:flex-row xl:!items-center">
|
||||
<div class="min-w-0 me-auto">
|
||||
<h1 class="box-title !text-lg">{{ title() }}</h1>
|
||||
@if (subtitle()) { <p class="mt-1 text-sm text-textmuted dark:text-textmuted/80">{{ subtitle() }}</p> }
|
||||
</div>
|
||||
<div class="flex w-full flex-wrap items-center gap-2 xl:w-auto">
|
||||
<label class="relative min-w-[14rem] flex-1 xl:flex-none">
|
||||
<span class="sr-only">Search</span>
|
||||
<input #searchInput type="search" class="form-control !pe-9 xl:w-64" [placeholder]="searchPlaceholder()"
|
||||
(input)="onSearch(searchInput.value)" />
|
||||
<i class="ri-search-line absolute end-3 top-1/2 -translate-y-1/2 text-textmuted"></i>
|
||||
</label>
|
||||
@if (showFilters()) {
|
||||
<button type="button" class="ti-btn ti-btn-light !mb-0" [class.!border-primary]="filtersOpen()"
|
||||
[attr.aria-expanded]="filtersOpen()" (click)="filtersOpen.set(!filtersOpen())">
|
||||
<i class="ri-filter-3-line"></i><span>Filters</span>
|
||||
</button>
|
||||
}
|
||||
<div class="inline-flex rounded-md shadow-sm" role="group" aria-label="View">
|
||||
<button type="button" class="ti-btn !mb-0 !rounded-e-none" [class.ti-btn-primary-full]="view() === 'table'" [class.ti-btn-light]="view() !== 'table'" (click)="setView('table')" title="Table view"><i class="ri-list-check-2"></i><span class="hidden sm:inline">Table</span></button>
|
||||
<button type="button" class="ti-btn !mb-0 !rounded-none" [class.ti-btn-primary-full]="view() === 'tile'" [class.ti-btn-light]="view() !== 'tile'" (click)="setView('tile')" title="Tile view"><i class="ri-layout-grid-line"></i><span class="hidden sm:inline">Tile</span></button>
|
||||
<button type="button" class="ti-btn !mb-0 !rounded-s-none" [class.ti-btn-primary-full]="view() === 'detail'" [class.ti-btn-light]="view() !== 'detail'" (click)="setView('detail')" title="Detail view"><i class="ri-layout-row-line"></i><span class="hidden sm:inline">Detail</span></button>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 text-sm"><span class="hidden sm:inline">Density</span>
|
||||
<select class="ti-form-select form-select-sm !w-auto" [value]="density()" (change)="setDensity($event)">
|
||||
<option value="compact">Compact</option><option value="comfortable">Comfortable</option><option value="spacious">Spacious</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" class="ti-btn ti-btn-primary-full !mb-0" (click)="addClicked.emit()"><i class="ri-add-line"></i>{{ addLabel() }}</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (filtersOpen()) {
|
||||
<div class="box-body border-t border-defaultborder">
|
||||
<ng-content select="[dataListFilters]" />
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button type="button" class="ti-btn ti-btn-light !mb-0" (click)="filtersCleared.emit()">Clear All</button>
|
||||
<button type="button" class="ti-btn ti-btn-primary-full !mb-0" (click)="filtersApplied.emit()">Apply Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="box custom-box !mb-0">
|
||||
@if (loading()) {
|
||||
<div class="box-body flex min-h-48 flex-col items-center justify-center gap-3"><i class="ti ti-loader-2 animate-spin text-2xl"></i><span>Loading data...</span></div>
|
||||
} @else if (totalRecords() === 0) {
|
||||
<div class="box-body flex min-h-48 flex-col items-center justify-center text-center"><i class="ri-inbox-2-line mb-3 text-4xl text-textmuted"></i><h2 class="font-semibold">{{ emptyMessage() }}</h2><p class="mt-1 text-sm text-textmuted">{{ emptyDescription() }}</p></div>
|
||||
} @else {
|
||||
<div [class.hidden]="view() !== 'table'"><ng-content select="[dataListTable]" /></div>
|
||||
<div [class.hidden]="view() !== 'tile'" class="box-body"><div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4" [class]="'grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 ' + contentSpacingClass()"><ng-content select="[dataListTile]" /></div></div>
|
||||
<div [class.hidden]="view() !== 'detail'" class="box-body"><div class="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3" [class]="'grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 ' + contentSpacingClass()"><ng-content select="[dataListDetail]" /></div></div>
|
||||
}
|
||||
<div class="box-footer flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
|
||||
<div class="flex flex-wrap items-center gap-3 text-sm"><span>Showing <b>{{ startRecord() }}</b> to <b>{{ endRecord() }}</b> of <b>{{ totalRecords() }}</b> entries</span>
|
||||
<label class="flex items-center gap-2">Show <select class="ti-form-select form-select-sm !w-[4.75rem]" [value]="pageSize()" (change)="onPageSizeChange($event)">@for (size of pageSizeOptions(); track size) { <option [value]="size">{{ size }}</option> }</select></label>
|
||||
</div>
|
||||
<nav aria-label="Pagination" class="pagination-style-4"><ul class="ti-pagination mb-0">
|
||||
<li class="page-item" [class.disabled]="pageIndex() <= 1"><button type="button" class="page-link" [disabled]="pageIndex() <= 1" (click)="goToPage(pageIndex() - 1)"><i class="ri-arrow-left-s-line"></i></button></li>
|
||||
@for (page of visiblePages(); track page) { <li class="page-item"><button type="button" class="page-link" [class.active]="page === pageIndex()" (click)="goToPage(page)">{{ page }}</button></li> }
|
||||
<li class="page-item" [class.disabled]="pageIndex() >= totalPages()"><button type="button" class="page-link" [disabled]="pageIndex() >= totalPages()" (click)="goToPage(pageIndex() + 1)"><i class="ri-arrow-right-s-line"></i></button></li>
|
||||
</ul></nav>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -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<string>();
|
||||
|
||||
readonly title = input.required<string>();
|
||||
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<readonly number[]>([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<string>();
|
||||
readonly addClicked = output<void>();
|
||||
readonly pageChanged = output<{ pageIndex: number; pageSize: number }>();
|
||||
readonly filtersCleared = output<void>();
|
||||
readonly filtersApplied = output<void>();
|
||||
|
||||
readonly view = signal<DataListView>('table');
|
||||
readonly density = signal<DataListDensity>('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 });
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -226,16 +226,9 @@
|
||||
Show
|
||||
</label>
|
||||
|
||||
<select id="dataTablePageSize"
|
||||
class="form-select form-select-sm !w-[80px] !py-1 !ps-2.5 !pe-6 text-defaulttextcolor bg-white dark:bg-bodybg border border-defaultborder rounded-md"
|
||||
[value]="pageSize()"
|
||||
(change)="onPageSizeChange($event)">
|
||||
@for (size of pageSizeOptions(); track size) {
|
||||
<option [value]="size" [selected]="size === pageSize()" class="text-defaulttextcolor bg-white dark:bg-bodybg">
|
||||
{{ size }}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
<p-select inputId="dataTablePageSize" [ngModel]="pageSize()" (ngModelChange)="onPageSizeChange($event)"
|
||||
[options]="pageSizeOptions()" appendTo="body" styleClass="data-table-page-size"
|
||||
panelStyleClass="data-table-page-size-panel" />
|
||||
|
||||
<span class="text-sm whitespace-nowrap text-defaulttextcolor">
|
||||
entries
|
||||
@@ -259,15 +252,21 @@
|
||||
|
||||
<!-- Page numbers -->
|
||||
@for (page of visiblePages(); track page) {
|
||||
@if (page === 'ellipsis-start' || page === 'ellipsis-end') {
|
||||
<li class="page-item page-item--ellipsis" aria-hidden="true">
|
||||
<span class="page-link px-2 py-[0.375rem]">…</span>
|
||||
</li>
|
||||
} @else {
|
||||
<li class="page-item" [class.active]="page === pageIndex()">
|
||||
<button type="button" class="page-link px-3 py-[0.375rem]" [class.active]="page === pageIndex()"
|
||||
[attr.aria-current]="
|
||||
page === pageIndex() ? 'page' : null
|
||||
" (click)="goToPage(page)">
|
||||
" (click)="goToPage($any(page))">
|
||||
{{ page }}
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Next -->
|
||||
<li class="page-item" [class.disabled]="pageIndex() >= totalPages()">
|
||||
|
||||
@@ -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 <body>, 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%;
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface DataTableStoreOptions<TItem, TRow extends DataTableRecord> {
|
||||
fetcher: (query: DataTableQuery) => Observable<DataTableResult<TItem>>;
|
||||
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<TItem, TRow extends DataTableRecord = TItem & DataTa
|
||||
readonly saving = signal<boolean>(false);
|
||||
|
||||
initialize(options: DataTableStoreOptions<TItem, TRow>): void {
|
||||
if (options.defaultSort) {
|
||||
this.queryState.setDefaultSort(options.defaultSort.column, options.defaultSort.direction ?? 'asc');
|
||||
}
|
||||
|
||||
this.querySubject$
|
||||
.pipe(
|
||||
debounceTime(50),
|
||||
|
||||
@@ -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<T extends DataTableRecord = DataTableRecord> {
|
||||
totalRecords = input(0);
|
||||
pageIndex = input(1);
|
||||
pageSize = input(15);
|
||||
pageSizeOptions = input<number[]>([10, 15, 20, 50]);
|
||||
pageSizeOptions = input<number[]>([10, 15, 20, 50, 100]);
|
||||
showPaginator = input(true);
|
||||
|
||||
/*---------------------------*/
|
||||
@@ -157,8 +161,24 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
|
||||
importClicked = output<void>();
|
||||
exportClicked = output<void>();
|
||||
|
||||
/** Column/direction the table should show as sorted on load (e.g. the store's current query sort). */
|
||||
initialSortColumn = input<string>('');
|
||||
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<string | number | null>(null);
|
||||
|
||||
tableClass = input<string>('table table-bordered whitespace-nowrap min-w-full');
|
||||
@@ -204,11 +224,15 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
|
||||
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<T extends DataTableRecord = DataTableRecord> {
|
||||
);
|
||||
}
|
||||
|
||||
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<T extends DataTableRecord = DataTableRecord> {
|
||||
});
|
||||
}
|
||||
|
||||
onPageSizeChange(event: Event): void {
|
||||
const pageSize = Number(
|
||||
(event.target as HTMLSelectElement).value
|
||||
);
|
||||
|
||||
onPageSizeChange(pageSize: number): void {
|
||||
if (!pageSize || pageSize === this.pageSize()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<p>form-checkbox works!</p>
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<p>form-file-upload works!</p>
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<p>form-radio works!</p>
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -273,10 +273,19 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
);
|
||||
});
|
||||
|
||||
private readonly optionLabelCache = new Map<TValue, FormSelectOption<TValue>>();
|
||||
|
||||
readonly selectItems = computed<readonly FormSelectOption<TValue>[]>(() => {
|
||||
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<TValue> => option !== undefined);
|
||||
const optionValues = new Set(selectedOptions.map(option => option.value));
|
||||
const visibleOptions = this.resolvedOptions()
|
||||
.filter(option => !optionValues.has(option.value));
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<p>form-textarea works!</p>
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -100,6 +100,9 @@
|
||||
<a class="side-menu__item" [routerLink]="!childrenItem.type ? null : [childrenItem.path] "
|
||||
routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}"
|
||||
(click)="setNavActive($event, childrenItem.path ?? '')">
|
||||
@if (childrenItem.icon) {
|
||||
<span [appSvgReplace]="childrenItem.icon" class="iconclick"></span>
|
||||
}
|
||||
{{childrenItem.title}}
|
||||
@if(childrenItem.badgeClass){
|
||||
|
||||
@@ -111,6 +114,9 @@
|
||||
<!-- empty -->
|
||||
@if (childrenItem.type === 'empty' ) {
|
||||
<a class="side-menu__item " href="javascript:;" (click)="setNavActive($event, childrenItem.path ?? '')">
|
||||
@if (childrenItem.icon) {
|
||||
<span [appSvgReplace]="childrenItem.icon" class="iconclick"></span>
|
||||
}
|
||||
{{childrenItem.title }}
|
||||
</a>
|
||||
}
|
||||
@@ -119,6 +125,9 @@
|
||||
<a class="side-menu__item " [ngClass]="{'active': childrenItem.selected}"
|
||||
[routerLink]="childrenItem.type ? null : [childrenItem.path]"
|
||||
(click)="toggleNavActive($event, childrenItem)">
|
||||
@if (childrenItem.icon) {
|
||||
<span [appSvgReplace]="childrenItem.icon" class="iconclick"></span>
|
||||
}
|
||||
<span class="">{{childrenItem.title}}</span>
|
||||
<i class="fe fe-chevron-right side-menu__angle"></i>
|
||||
</a>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user