feat: add Master Admin application functionality

This commit is contained in:
Gagan7900
2026-07-24 11:55:27 +05:30
parent 10038a52cf
commit d514d09879
224 changed files with 34594 additions and 958 deletions
@@ -0,0 +1,43 @@
import { buildApiUrl } from '../../../../core/config/api-url.util';
export const STATE_ENDPOINTS = {
dataTable: buildApiUrl(
'masterAdmin',
'/v1/states/datatable'
),
create: buildApiUrl(
'masterAdmin',
'/v1/states'
),
getById: (id: string) =>
buildApiUrl(
'masterAdmin',
`/v1/states/${encodeURIComponent(id)}`
),
autocomplete: buildApiUrl(
'masterAdmin',
'/v1/states/autocomplete'
),
update: (id: string) =>
buildApiUrl(
'masterAdmin',
`/v1/states/${encodeURIComponent(id)}`
),
delete: (id: string) =>
buildApiUrl(
'masterAdmin',
`/v1/states/${encodeURIComponent(id)}`
),
changeStatus: (id: string) =>
buildApiUrl(
'masterAdmin',
`/v1/states/${encodeURIComponent(id)}/status`
),
} as const;
@@ -0,0 +1,45 @@
import { HttpClient, HttpParams } from "@angular/common/http";
import { Injectable, inject } from "@angular/core";
import { STATE_ENDPOINTS } from "./state.endpoints"
import { Observable } from "rxjs";
import { DataTableQuery, DataTableResult } from "../../../../shared/components/data-table/data-table.types";
import {
CreateStateRequest,
StateDto,
StateLookupDto,
UpdateStateRequest
} from "../models/state.model";
@Injectable({
providedIn: 'root'
})
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;
return this.http.post<DataTableResult<StateDto>>(`${STATE_ENDPOINTS.dataTable}`, query, { params });
}
createState(request: CreateStateRequest): Observable<StateDto> {
return this.http.post<StateDto>(STATE_ENDPOINTS.create, request);
}
updateState(id: string, request: UpdateStateRequest): Observable<StateDto> {
return this.http.put<StateDto>(STATE_ENDPOINTS.update(id), request);
}
getStateById(id: string): Observable<StateDto> {
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)
});
}
}
@@ -0,0 +1,30 @@
export interface StateDto {
id: string;
countryId: string;
name: string;
code: string | null;
isActive: boolean;
createdOn?: string;
modifiedOn?: string | null;
}
export interface StateLookupDto {
id: string;
name: string;
code: string;
}
export interface CreateStateRequest {
countryId: string;
name: string;
code: string;
}
export interface UpdateStateRequest {
countryId: null;
name: string;
code: string;
isActive: boolean;
}
export type StateModalMode = 'create' | 'edit';
@@ -1,19 +1,104 @@
<app-data-table
[columns]="columns()"
[rows]="states()"
[actions]="actions()"
[loading]="loading()"
[totalRecords]="totalRecords()"
[pageIndex]="queryState.pageIndex()"
[pageSize]="queryState.pageSize()"
[pageSizeOptions]="[5, 10, 20, 50]"
title="States"
[showSearch]="true"
searchPlaceholder="Search states..."
[searchDebounceTime]="300"
<div class="grid grid-cols-12 gap-6">
<div class="xl:col-span-12 col-span-12">
<app-filter-card title="Filter" titleIcon="ti ti-filter" headerClass="!py-2" bodyClass="!px-4 !py-2.5">
<form [formGroup]="countryFilterForm" autocomplete="off" class="grid w-full grid-cols-12 items-end gap-3">
<div class="col-span-12 sm:col-span-5 lg:col-span-2">
<app-autocomplete
formControlName="countryId"
inputId="state-country-filter"
variant="floating"
size="sm"
label="Country"
placeholder="Search"
[searchFn]="searchCountries"
[displayWith]="displayCountry"
[valueWith]="countryValue"
[selectedItem]="selectedCountryLookup()"
[minSearchLength]="1"
[debounceTime]="300"
[limit]="50"
[clearable]="true"
[hideValidation]="true"
wrapperClass="!mb-0 w-full"
(itemSelected)="onCountryLookupSelected($event)"
/>
</div>
<div class="col-span-12 sm:col-span-3 lg:col-span-1">
<button type="button" class="ti-btn ti-btn-sm ti-btn-primary-full !mb-0 flex min-h-8 w-full items-center justify-center gap-1.5 !px-3 md:!w-auto" (click)="applyCountryFilter()">
<i class="ti ti-filter" aria-hidden="true"></i>
<span>Filter</span>
</button>
</div>
</form>
</app-filter-card>
</div>
</div>
(searchChanged)="onSearch($event)"
(pageChanged)="onPageChange($event)"
(sortChanged)="onSortChange($event)"
(actionClicked)="onActionClick($event)">
</app-data-table>
<app-data-table [columns]="columns()" [rows]="states()" [actions]="actions()"
[totalRecords]="totalRecords()" [pageIndex]="queryState.pageIndex()" [pageSize]="queryState.pageSize()"
tableTitle="States" buttonTitle="Add" [showSearch]="true"
[showAddButton]="true" searchPlaceholder="Search..." [searchDebounceTime]="300"
[emptyMessage]="emptyMessage()" [emptyDescription]="emptyDescription()"
(addClicked)="onAddState()" (searchChanged)="onSearch($event)" (pageChanged)="onPageChange($event)"
(sortChanged)="onSortChange($event)" (actionClicked)="onActionClick($event)" toolTip="Add State" />
<app-confirm-dialog
title="Delete State"
text="Do you really want to delete this state?"
confirmButtonText="Delete"
cancelButtonText="Cancel"
(confirmed)="onDeleteConfirmed()"
(cancelled)="onDeleteCancelled()"
/>
<modal [open]="showStateModal()" [title]="stateModalTitle()" size="md" [submitAction]="stateSubmitAction()"
[submitLabel]="stateSubmitLabel()" [loadingLabel]="stateLoadingLabel()" [loading]="saving()"
(closed)="closeStateModal()" (submitted)="saveState()">
<form [formGroup]="stateForm" (ngSubmit)="saveState()" autocomplete="off">
<div class="grid grid-cols-12 gap-x-5 gap-y-5">
<div class="col-span-12 md:col-span-6">
<app-autocomplete
formControlName="countryId"
inputId="state-country"
variant="floating"
label="Country"
placeholder="Search"
[searchFn]="searchCountries"
[displayWith]="displayCountry"
[valueWith]="countryValue"
[resolveValueFn]="resolveCountry"
[selectedItem]="selectedFormCountry()"
[minSearchLength]="1"
[debounceTime]="300"
[limit]="50"
[clearable]="stateModalMode() === 'create'"
[required]="true"
[clearable]="true"
[readonly]="stateModalMode() !== 'create'"
[validationMessages]="{ required: 'Country is required.' }"
[submitAttempted]="stateSubmitAttempted()"
wrapperClass="w-full"
(itemSelected)="onFormCountrySelected($event)"
(cleared)="onFormCountryCleared()"
/>
</div>
<div class="col-span-12 md:col-span-6">
<app-form-input formControlName="name" inputId="state-name" label="State Name" placeholder="Name" variant="floating"
autocomplete="off" [required]="true" [maxLength]="150" [validationMessages]="{
required: 'State Name is required.',
maxlength: 'State Name cannot exceed 150 characters.'
}" [submitAttempted]="stateSubmitAttempted()" />
</div>
<div class="col-span-12 md:col-span-6">
<app-form-input formControlName="code" inputId="state-code" label="State Code" placeholder="e.g.: CA" variant="floating"
autocomplete="off" [required]="true" [maxLength]="16" [validationMessages]="{
required: 'State Code is required.',
maxlength: 'State Code cannot exceed 16 characters.',
pattern: 'State Code can contain letters, numbers, hyphens, and underscores only.'
}" [submitAttempted]="stateSubmitAttempted()" />
</div>
</div>
</form>
</modal>
@@ -1,50 +1,187 @@
import { Component } from '@angular/core';
import { inject, signal } from '@angular/core';
import { StateService } from '../../../../../core/services/state/state.service';
import { Component, DestroyRef, ElementRef, computed, inject, signal, viewChild } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
FormBuilder,
ReactiveFormsModule,
Validators
} from '@angular/forms';
import { ToastrService } from 'ngx-toastr';
import { Subject, catchError, distinctUntilChanged, finalize, map, of, switchMap } from 'rxjs';
import {
CountryLookupDto
} from '../../../countries/public-api';
import {
CreateStateRequest,
StateDto,
StateModalMode,
UpdateStateRequest
} from '../../models/state.model';
import { CountryService } from '../../../countries/public-api';
import { StateService } from '../../data-access/state.service';
import { DataTableQueryState } from '../../../../../shared/components/data-table/data-table-query.state';
import { DataTablePageEvent, DataTableSortEvent, DataTableQuery, DataTableColumn, DataTableAction, DataTableActionEvent } from '../../../../../shared/components/data-table/data-table.types';
import {
DataTableAction,
DataTableActionEvent,
DataTableColumn,
DataTablePageEvent,
DataTableQuery,
DataTableRecord,
DataTableSortEvent
} from '../../../../../shared/components/data-table/data-table.types';
import { DataTable } from '../../../../../shared/components/data-table/data-table';
import { finalize} from 'rxjs/operators';
import { Autocomplete } from '../../../../../shared/components/form/autocomplete/autocomplete';
import {
AutocompleteDisplayFn,
AutocompleteResolveValueFn,
AutocompleteSearchFn,
AutocompleteValueFn
} from '../../../../../shared/components/form/autocomplete/autocomplete.types';
import { FormInput } from '../../../../../shared/components/form/form-input/form-input';
import { Modal } from '../../../../../shared/components/modal/modal';
import { ConfirmDialog } from '../../../../../shared/components/confirm-dialog/confirm-dialog';
import { FilterCard } from '../../../../../shared/components/filter-card/filter-card';
interface StateTableRow extends DataTableRecord {
id: string;
countryId: string;
name: string;
code: string | null;
isActive: boolean;
serialNumber: number;
createdOn?: string;
modifiedOn?: string | null;
}
@Component({
selector: 'state-list',
imports: [DataTable],
standalone: true,
imports: [DataTable, Modal, ReactiveFormsModule, FormInput, Autocomplete, ConfirmDialog, FilterCard],
templateUrl: './state-list.html',
styleUrl: './state-list.scss',
})
export class StateList {
private readonly stateApi: StateService = inject(StateService);
private readonly destroyRef = inject(DestroyRef);
private readonly stateApi = inject(StateService);
private readonly countryApi = inject(CountryService);
private readonly formBuilder = inject(FormBuilder);
private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
private readonly toastr = inject(ToastrService);
private readonly stateQueryRequests$ = new Subject<DataTableQuery>();
readonly queryState = new DataTableQueryState();
readonly states = signal<any[]>([]);
readonly states = signal<StateTableRow[]>([]);
readonly selectedCountryLookup = signal<CountryLookupDto | null>(null);
readonly selectedFormCountry = signal<CountryLookupDto | null>(null);
readonly selectedCountryId = signal<string | null>(null);
readonly appliedCountryId = signal<string | null>(null);
readonly totalRecords = signal(0);
readonly filteredRecords = signal(0);
readonly loading = signal(false);
readonly saving = signal(false);
readonly showStateModal = signal(false);
readonly stateModalMode = signal<StateModalMode>('create');
readonly selectedStateId = signal<string | null>(null);
readonly selectedState = signal<StateDto | null>(null);
readonly stateSubmitAttempted = signal(false);
readonly pendingDeleteState = signal<StateDto | null>(null);
readonly deleteConfirmDialog = viewChild(ConfirmDialog);
readonly columns = signal<DataTableColumn[]>([
{ key: 'serialNumber', label: 'Sr.No.', header: 'Sr.No.', sortable: false, width: '60px' },
{ key: 'name', header: 'Name', label: 'Name', sortable: true },
readonly countryFilterForm = this.formBuilder.nonNullable.group({
countryId: ['']
});
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 stateForm = this.formBuilder.nonNullable.group({
countryId: [
'',
[
Validators.required
]
],
name: [
'',
[
Validators.required,
Validators.maxLength(150)
]
],
code: [
'',
[
Validators.required,
Validators.maxLength(16),
Validators.pattern(/^[A-Za-z0-9_-]+$/)
]
]
});
readonly emptyMessage = computed(() =>
this.appliedCountryId()
? 'No states found'
: 'No records found'
);
readonly emptyDescription = computed(() =>
this.appliedCountryId()
? 'There are no states available for the selected country.'
: 'There is currently no data to display.'
);
readonly stateModalTitle = computed(() =>
this.stateModalMode() === 'create'
? 'Add State'
: 'Edit State'
);
readonly stateSubmitLabel = computed(() =>
this.stateModalMode() === 'create'
? 'Save'
: 'Update'
);
readonly stateLoadingLabel = computed(() =>
this.stateModalMode() === 'create'
? 'Saving...'
: 'Updating...'
);
readonly stateSubmitAction = computed<'save' | 'update'>(() =>
this.stateModalMode() === 'create'
? 'save'
: 'update'
);
readonly columns = signal<DataTableColumn<StateTableRow>[]>([
{ key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '60px' },
{ key: 'name', header: 'Name', label: 'Name', sortable: true, align: 'left' },
{ key: 'code', header: 'Code', label: 'Code', sortable: true },
{ key: 'isActive', label: 'Status', header: 'Status', sortable: true, badge: true, badgeClass: value =>
value === true
? 'badge bg-success/10 text-success'
: 'badge bg-danger/10 text-danger',
{
key: 'isActive',
label: 'Status',
header: 'Status',
sortable: true,
badge: true,
badgeClass: value =>
value === true
? 'badge bg-success/10 text-success'
: 'badge bg-danger/10 text-danger',
width: '100px',
formatter: (value) => value ? 'Active' : 'Inactive'
formatter: value => value ? 'Active' : 'Inactive'
}
]);
readonly actions = signal<DataTableAction[]>([
{
type: 'view',
label: 'View',
icon: 'ti ti-eye',
className: 'text-info'
},
readonly actions = signal<DataTableAction<StateTableRow>[]>([
{
type: 'edit',
label: 'Edit',
@@ -56,70 +193,107 @@ export class StateList {
label: 'Delete',
icon: 'ti ti-trash',
className: 'text-danger',
visible: (row: any) => row.isActive
visible: row => row.isActive
},
{
type: 'activate',
label: 'Activate',
icon: 'ti ti-check',
className: 'text-success',
visible: (row: any) => !row.isActive
visible: row => !row.isActive
}
]);
ngOnInit(): void {
this.loadStates(this.queryState.getQuery(), 'a9d18090-f76a-489f-bfc7-9d97d3d2d7da');
}
loadStates(query: DataTableQuery, countryId: any): void {
this.loading.set(true);
this.stateApi
.getStateDataTable(query, countryId)
constructor() {
this.countryFilterForm.controls.countryId.valueChanges
.pipe(
finalize(() => {
this.loading.set(false);
})
distinctUntilChanged(),
takeUntilDestroyed(this.destroyRef)
)
.subscribe({
next: (response: any) => {
console.log('State data loaded:', response);
if (response.draw !== this.queryState.getQuery().draw) {
return;
}
// Add serial numbers to states
const statesWithSerialNumbers = response.rows.map((state: any, index: number) => ({
...state,
serialNumber: (query.page - 1) * query.pageSize + index + 1
}));
this.states.set(statesWithSerialNumbers);
this.totalRecords.set(response.total);
this.filteredRecords.set(response.filtered);
},
error: (error: any) => {
console.error('Unable to load states.', error);
this.states.set([]);
this.totalRecords.set(0);
this.filteredRecords.set(0);
}
.subscribe(countryId => {
this.onCountrySelected(countryId || null);
});
this.stateQueryRequests$
.pipe(
switchMap(query => {
const countryId = this.appliedCountryId();
return this.stateApi.getStateDataTable(query, countryId).pipe(
catchError(() => {
this.toastr.error('Unable to load states.');
this.clearStateGrid();
return of(null);
})
);
}),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(response => {
if (!response) {
return;
}
const query = this.queryState.getQuery();
if (response.draw !== query.draw) {
return;
}
const statesWithSerialNumbers: StateTableRow[] = response.rows.map((state, index) => ({
...state,
serialNumber: (query.page - 1) * query.pageSize + index + 1
}));
this.states.set(statesWithSerialNumbers);
this.totalRecords.set(response.total);
this.filteredRecords.set(response.filtered);
});
}
ngOnInit(): void {
this.loadStates(this.queryState.getQuery());
}
loadStates(query: DataTableQuery): void {
this.stateQueryRequests$.next(query);
}
onCountryLookupSelected(country: CountryLookupDto): void {
this.selectedCountryLookup.set(country);
}
applyCountryFilter(): void {
const countryId = this.countryFilterForm.controls.countryId.value || null;
this.appliedCountryId.set(countryId);
this.loadStates(this.queryState.setPage({
pageIndex: 1,
pageSize: this.queryState.pageSize()
}));
}
onFormCountrySelected(country: CountryLookupDto): void {
this.selectedFormCountry.set(country);
}
onFormCountryCleared(): void {
this.selectedFormCountry.set(null);
}
onSearch(value: string): void {
const query = this.queryState.setSearch(value.trim());
this.loadStates(query, 'a9d18090-f76a-489f-bfc7-9d97d3d2d7da');
this.loadStates(query);
}
onPageChange(event: DataTablePageEvent): void {
const query = this.queryState.setPage(event);
this.loadStates(query, 'a9d18090-f76a-489f-bfc7-9d97d3d2d7da');
this.loadStates(query);
}
onSortChange(event: DataTableSortEvent): void {
const query = this.queryState.setSort(event);
this.loadStates(query, 'a9d18090-f76a-489f-bfc7-9d97d3d2d7da');
this.loadStates(query);
}
onRefresh(): void {
@@ -130,27 +304,42 @@ export class StateList {
draw: currentQuery.draw + 1
};
this.loadStates(query, 'a9d18090-f76a-489f-bfc7-9d97d3d2d7da');
this.loadStates(query);
}
onReset(): void {
const query = this.queryState.reset();
this.loadStates(query, 'a9d18090-f76a-489f-bfc7-9d97d3d2d7da');
this.loadStates(query);
}
onActionClick(event: DataTableActionEvent): void {
onDeleteConfirmed(): void {
const state = this.pendingDeleteState();
if (!state) {
return;
}
this.pendingDeleteState.set(null);
this.deleteState(state);
}
onDeleteCancelled(): void {
this.pendingDeleteState.set(null);
}
onActionClick(event: DataTableActionEvent<StateTableRow>): void {
const action = event.action.type;
const state = event.row;
const state = this.toStateDto(event.row);
switch (action) {
case 'view':
this.viewState(state);
break;
case 'edit':
this.editState(state);
this.openEditState(state);
break;
case 'delete':
this.deleteState(state);
this.requestDeleteState(state);
break;
case 'activate':
this.activateState(state);
@@ -158,24 +347,224 @@ export class StateList {
}
}
private viewState(state: any): void {
console.log('Viewing state:', state);
// TODO: Implement view logic (open modal, navigate to details page, etc.)
onAddState(): void {
this.stateModalMode.set('create');
this.selectedStateId.set(null);
this.selectedState.set(null);
this.stateSubmitAttempted.set(false);
this.selectedFormCountry.set(null);
this.stateForm.reset({
countryId: '',
name: '',
code: ''
});
this.resetStateFormState();
this.showStateModal.set(true);
}
private editState(state: any): void {
console.log('Editing state:', state);
// TODO: Implement edit logic (open modal, navigate to edit page, etc.)
closeStateModal(): void {
if (this.saving()) {
return;
}
this.showStateModal.set(false);
this.selectedStateId.set(null);
this.selectedState.set(null);
this.selectedFormCountry.set(null);
this.stateSubmitAttempted.set(false);
}
private deleteState(state: any): void {
console.log('Deleting state:', state);
// TODO: Implement delete logic (API call to delete state)
saveState(): void {
if (this.stateForm.invalid) {
this.stateSubmitAttempted.set(true);
this.stateForm.markAllAsTouched();
this.focusFirstInvalidStateControl();
return;
}
if (this.saving()) {
return;
}
this.saving.set(true);
if (this.stateModalMode() === 'create') {
this.stateApi
.createState(this.buildCreateStateRequest())
.pipe(finalize(() => this.saving.set(false)))
.subscribe({
next: () => {
this.toastr.success('State saved successfully.');
this.finishStateSave();
}
});
return;
}
const stateId = this.selectedStateId();
if (!stateId) {
this.saving.set(false);
return;
}
this.stateApi
.updateState(
stateId,
this.buildUpdateStateRequest(this.selectedState()?.isActive ?? true)
)
.pipe(finalize(() => this.saving.set(false)))
.subscribe({
next: () => {
this.toastr.success('State updated successfully.');
this.finishStateSave();
}
});
}
private activateState(state: any): void {
console.log('Activating state:', state);
// TODO: Implement activate logic (API call to activate state)
private onCountrySelected(countryId: string | null): void {
this.selectedCountryId.set(countryId);
if (!countryId || this.selectedCountryLookup()?.id !== countryId) {
this.selectedCountryLookup.set(null);
}
}
private clearStateGrid(): void {
this.states.set([]);
this.totalRecords.set(0);
this.filteredRecords.set(0);
}
private viewState(state: StateDto): void {
this.openEditState(state);
}
private openEditState(state: StateDto): void {
this.stateModalMode.set('edit');
this.selectedStateId.set(state.id);
this.selectedState.set(state);
this.stateSubmitAttempted.set(false);
this.stateApi
.getStateById(state.id)
.subscribe({
next: stateDetails => {
this.selectedState.set(stateDetails);
this.selectedFormCountry.set(null);
this.stateForm.reset({
countryId: stateDetails.countryId ?? '',
name: stateDetails.name ?? '',
code: stateDetails.code ?? ''
});
this.resetStateFormState();
this.showStateModal.set(true);
}
});
}
private requestDeleteState(state: StateDto): void {
this.pendingDeleteState.set(state);
this.deleteConfirmDialog()?.open();
}
private deleteState(state: StateDto): void {
this.updateStateStatus(state, false);
}
private activateState(state: StateDto): void {
this.updateStateStatus(state, true);
}
private buildCreateStateRequest(): CreateStateRequest {
const value = this.stateForm.getRawValue();
return {
countryId: value.countryId,
name: value.name.trim(),
code: value.code.trim().toUpperCase()
};
}
private buildUpdateStateRequest(isActive: boolean): UpdateStateRequest {
const value = this.stateForm.getRawValue();
return {
countryId: null,
name: value.name.trim(),
code: value.code.trim().toUpperCase(),
isActive
};
}
private stateToUpdateRequest(state: StateDto, isActive: boolean): UpdateStateRequest {
return {
countryId: null,
name: state.name?.trim() ?? '',
code: state.code?.trim().toUpperCase() ?? '',
isActive
};
}
private updateStateStatus(state: StateDto, isActive: boolean): void {
this.stateApi
.updateState(state.id, this.stateToUpdateRequest(state, isActive))
.subscribe({
next: () => {
this.toastr.success(
isActive
? 'State activated successfully.'
: 'State deactivated successfully.'
);
this.loadStates(this.queryState.getQuery());
}
});
}
private resetStateFormState(): void {
this.stateForm.markAsPristine();
this.stateForm.markAsUntouched();
this.stateForm.updateValueAndValidity();
}
private finishStateSave(): void {
this.showStateModal.set(false);
this.selectedStateId.set(null);
this.selectedState.set(null);
this.selectedFormCountry.set(null);
this.stateSubmitAttempted.set(false);
this.loadStates(this.queryState.getQuery());
}
private focusFirstInvalidStateControl(): void {
queueMicrotask(() => {
const firstInvalidControl =
this.elementRef.nativeElement.querySelector<HTMLElement>(
'modal [data-form-control][aria-invalid="true"]'
);
firstInvalidControl?.focus();
firstInvalidControl?.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
});
}
private toStateDto(row: StateTableRow): StateDto {
return {
id: row.id,
countryId: row.countryId,
name: row.name,
code: row.code,
isActive: row.isActive,
createdOn: row.createdOn,
modifiedOn: row.modifiedOn
};
}
}
@@ -0,0 +1,2 @@
export { StateService } from './data-access/state.service';
export type { StateLookupDto } from './models/state.model';