-
+
-
-
diff --git a/src/app/features/global-masters/currencies/pages/currency-list/currency-list.ts b/src/app/features/global-masters/currencies/pages/currency-list/currency-list.ts
index a749e989..cfeeb807 100644
--- a/src/app/features/global-masters/currencies/pages/currency-list/currency-list.ts
+++ b/src/app/features/global-masters/currencies/pages/currency-list/currency-list.ts
@@ -1,5 +1,11 @@
import { Component, DestroyRef, ElementRef, computed, inject, signal, viewChild } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import {
+ CdkConnectedOverlay,
+ CdkOverlayOrigin,
+ ConnectedOverlayPositionChange,
+ ConnectedPosition
+} from '@angular/cdk/overlay';
import {
FormBuilder,
ReactiveFormsModule,
@@ -10,11 +16,13 @@ import { Subject, catchError, finalize, of, switchMap } from 'rxjs';
import {
CreateCurrencyRequest,
+ CurrencyCountryFlag,
CurrencyDto,
+ CurrencyIso2Value,
CurrencyModalMode,
UpdateCurrencyRequest
-} from '../../../../../core/models/currency/currency.model';
-import { CurrencyService } from '../../../../../core/services/currency/currency.service';
+} from '../../models/currency.model';
+import { CurrencyService } from '../../data-access/currency.service';
import { DataTableQueryState } from '../../../../../shared/components/data-table/data-table-query.state';
import {
DataTableAction,
@@ -27,14 +35,15 @@ import {
} from '../../../../../shared/components/data-table/data-table.types';
import { DataTable } from '../../../../../shared/components/data-table/data-table';
import { DataTableCellDirective } from '../../../../../shared/directives/data-table-cell.directive';
-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 { FormInput } from '../../../../../shared/components/form/form-input/form-input';
+type Iso2TooltipPlacement = 'above' | 'below' | 'left' | 'right';
interface CurrencyTableRow extends DataTableRecord {
id: string;
code: string;
- iso2: string;
+ iso2: CurrencyIso2Value;
name: string;
symbol: string;
numericCode: number;
@@ -48,7 +57,7 @@ interface CurrencyTableRow extends DataTableRecord {
@Component({
selector: 'currency-list',
standalone: true,
- imports: [DataTable, DataTableCellDirective, Modal, ReactiveFormsModule, FormInput, ConfirmDialog],
+ imports: [DataTable, DataTableCellDirective, Modal, ReactiveFormsModule, FormInput, ConfirmDialog, CdkOverlayOrigin, CdkConnectedOverlay],
templateUrl: './currency-list.html',
styleUrl: './currency-list.scss',
})
@@ -74,6 +83,18 @@ export class CurrencyList {
readonly currencySubmitAttempted = signal(false);
readonly pendingDeleteCurrency = signal
(null);
readonly deleteConfirmDialog = viewChild(ConfirmDialog);
+ readonly openIso2TooltipCurrencyId = signal(null);
+ readonly iso2TooltipPlacement = signal('right');
+ readonly iso2TooltipPositions: ConnectedPosition[] = [
+ {
+ originX: 'end',
+ originY: 'center',
+ overlayX: 'start',
+ overlayY: 'center',
+ offsetX: 12
+ }
+ ];
+ private iso2TooltipCloseTimer: ReturnType | null = null;
readonly currencyForm = this.formBuilder.nonNullable.group({
name: [
@@ -144,7 +165,7 @@ export class CurrencyList {
readonly columns = signal[]>([
{ key: 'serialNumber', label: 'Sr. No.', header: 'Sr.No.', sortable: false, width: '100px' },
{ key: 'name', label: 'Name', header: 'Name', sortable: true, align: 'left' },
- { key: 'iso2', label: 'Iso2 Code', header: 'Iso2 Code', sortable: true },
+ { key: 'iso2', label: 'Iso2 Code', header: 'Iso2 Code', sortable: true , align: 'left'},
{ key: 'code', label: 'Code', header: 'Code', sortable: true },
{ key: 'symbol', label: 'Symbol', header: 'Symbol', sortable: true },
{ key: 'numericCode', label: 'Numeric Code', header: 'Numeric Code', sortable: true },
@@ -390,7 +411,7 @@ export class CurrencyList {
name: currencyDetails.name ?? '',
code: currencyDetails.code ?? '',
symbol: currencyDetails.symbol ?? '',
- numericCode: currencyDetails.numericCode ?? 0,
+ numericCode: currencyDetails.numericCode ?? '0',
decimalDigits: currencyDetails.decimalDigits ?? 2
});
this.resetCurrencyFormState();
@@ -482,7 +503,7 @@ export class CurrencyList {
queueMicrotask(() => {
const firstInvalidControl =
this.elementRef.nativeElement.querySelector(
- 'modal .form-control.is-invalid, modal [aria-invalid="true"]'
+ 'modal [data-form-control][aria-invalid="true"]'
);
firstInvalidControl?.focus();
@@ -497,7 +518,7 @@ export class CurrencyList {
return {
id: row.id,
code: row.code,
- iso2: row.iso2?.toLowerCase() ?? '',
+ iso2: row.iso2,
name: row.name,
symbol: row.symbol,
numericCode: row.numericCode,
@@ -508,8 +529,87 @@ export class CurrencyList {
};
}
- getFlagUrl(iso2: string | null | undefined): string {
- const code = iso2?.trim().toLowerCase();
+ visibleCountries(row: CurrencyTableRow): readonly CurrencyCountryFlag[] {
+ return this.normalizeIso2Codes(row.iso2).slice(0, 1);
+ }
+
+ remainingCountries(row: CurrencyTableRow): readonly CurrencyCountryFlag[] {
+ return this.normalizeIso2Codes(row.iso2).slice(1);
+ }
+
+ remainingCountryCount(row: CurrencyTableRow): number {
+ return this.remainingCountries(row).length;
+ }
+
+ openIso2Tooltip(row: CurrencyTableRow): void {
+ this.cancelIso2TooltipClose();
+ this.openIso2TooltipCurrencyId.set(row.id);
+ }
+
+ scheduleIso2TooltipClose(): void {
+ this.cancelIso2TooltipClose();
+ this.iso2TooltipCloseTimer = setTimeout(() => this.closeIso2Tooltip(), 120);
+ }
+
+ isIso2TooltipOpen(row: CurrencyTableRow): boolean {
+ return this.openIso2TooltipCurrencyId() === row.id;
+ }
+
+ iso2TooltipId(row: CurrencyTableRow): string {
+ return `currency-iso2-tooltip-${row.id}`;
+ }
+
+ closeIso2Tooltip(): void {
+ this.cancelIso2TooltipClose();
+ this.openIso2TooltipCurrencyId.set(null);
+ }
+
+ onIso2TooltipKeydown(event: KeyboardEvent): void {
+ if (event.key === 'Escape') {
+ event.preventDefault();
+ this.closeIso2Tooltip();
+ }
+ }
+
+ onIso2TooltipPositionChange(event: ConnectedOverlayPositionChange): void {
+ this.iso2TooltipPlacement.set(
+ event.connectionPair.overlayY === 'bottom' ? 'above' : 'below'
+ );
+ }
+
+ normalizeIso2Codes(value: unknown): CurrencyCountryFlag[] {
+ const items: readonly unknown[] = Array.isArray(value)
+ ? value
+ : typeof value === 'string'
+ ? value.split(/[,;|]/)
+ : [];
+ const codes = new Set();
+
+ for (const item of items) {
+ const rawCode = typeof item === 'string'
+ ? item
+ : item && typeof item === 'object' && 'iso2' in item
+ ? String(item.iso2)
+ : '';
+ const iso2 = rawCode.trim().toUpperCase();
+
+ if (/^[A-Z]{2}$/.test(iso2)) {
+ codes.add(iso2);
+ }
+ }
+
+ return [...codes].map(iso2 => ({ iso2 }));
+ }
+
+ private cancelIso2TooltipClose(): void {
+ if (this.iso2TooltipCloseTimer !== null) {
+ clearTimeout(this.iso2TooltipCloseTimer);
+ this.iso2TooltipCloseTimer = null;
+ }
+ }
+
+ getFlagUrl(value: unknown): string {
+ const code = this.normalizeIso2Codes(value)[0]?.iso2.toLowerCase();
return code && /^[a-z]{2}$/.test(code)
? `https://flagcdn.com/24x18/${code}.png`
@@ -517,6 +617,7 @@ export class CurrencyList {
}
onFlagError(event: Event): void {
const image = event.target as HTMLImageElement;
- image.style.display = 'none';
+ image.classList.add('hidden');
+ image.nextElementSibling?.classList.remove('hidden');
}
}
diff --git a/src/app/features/global-masters/currencies/public-api.ts b/src/app/features/global-masters/currencies/public-api.ts
new file mode 100644
index 00000000..ed036285
--- /dev/null
+++ b/src/app/features/global-masters/currencies/public-api.ts
@@ -0,0 +1,2 @@
+export { CurrencyService } from './data-access/currency.service';
+export type { CurrencyDto, CurrencyLookupDto } from './models/currency.model';
diff --git a/src/app/features/global-masters/languages/data-access/language.endpoints.ts b/src/app/features/global-masters/languages/data-access/language.endpoints.ts
new file mode 100644
index 00000000..123ad1fa
--- /dev/null
+++ b/src/app/features/global-masters/languages/data-access/language.endpoints.ts
@@ -0,0 +1,11 @@
+import { buildApiUrl } from '../../../../core/config/api-url.util';
+
+export const LANGUAGE_ENDPOINTS = {
+ dataTable: buildApiUrl('masterAdmin', '/v1/languages/datatable'),
+ create: buildApiUrl('masterAdmin', '/v1/languages'),
+ getById: (id: string) =>
+ buildApiUrl('masterAdmin', `/v1/languages/${encodeURIComponent(id)}`),
+ update: (id: string) =>
+ buildApiUrl('masterAdmin', `/v1/languages/${encodeURIComponent(id)}`),
+ autocomplete: buildApiUrl('masterAdmin', '/v1/languages/autocomplete')
+} as const;
diff --git a/src/app/features/global-masters/languages/data-access/language.service.ts b/src/app/features/global-masters/languages/data-access/language.service.ts
new file mode 100644
index 00000000..d491b95f
--- /dev/null
+++ b/src/app/features/global-masters/languages/data-access/language.service.ts
@@ -0,0 +1,48 @@
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable, inject } from '@angular/core';
+import { Observable } from 'rxjs';
+
+import { LANGUAGE_ENDPOINTS } from './language.endpoints';
+import {
+ CreateLanguageRequest,
+ LanguageDto,
+ LanguageLookupDto,
+ UpdateLanguageRequest
+} from '../models/language.model';
+import {
+ DataTableQuery,
+ DataTableResult
+} from '../../../../shared/components/data-table/data-table.types';
+
+@Injectable({ providedIn: 'root' })
+export class LanguageService {
+ private readonly http = inject(HttpClient);
+
+ getDataTable(query: DataTableQuery): Observable> {
+ return this.http.post>(LANGUAGE_ENDPOINTS.dataTable, query);
+ }
+
+ getById(id: string): Observable {
+ return this.http.get(LANGUAGE_ENDPOINTS.getById(id));
+ }
+
+ create(request: CreateLanguageRequest): Observable {
+ return this.http.post(LANGUAGE_ENDPOINTS.create, request);
+ }
+
+ update(id: string, request: UpdateLanguageRequest): Observable {
+ return this.http.put(LANGUAGE_ENDPOINTS.update(id), request);
+ }
+
+ autocomplete(
+ term: string | null,
+ limit = 10
+ ): Observable {
+ let params = new HttpParams().set('limit', limit);
+ if (term !== null) {
+ params = params.set('term', term);
+ }
+
+ return this.http.get(LANGUAGE_ENDPOINTS.autocomplete, { params });
+ }
+}
diff --git a/src/app/features/global-masters/languages/models/language.model.ts b/src/app/features/global-masters/languages/models/language.model.ts
new file mode 100644
index 00000000..fc1f7539
--- /dev/null
+++ b/src/app/features/global-masters/languages/models/language.model.ts
@@ -0,0 +1,31 @@
+export interface LanguageDto {
+ id: string;
+ code: string;
+ name: string;
+ nativeName: string;
+ isRightToLeft: boolean;
+ isActive: boolean;
+ createdOn: string;
+ modifiedOn: string | null;
+}
+
+export interface LanguageLookupDto {
+ readonly id: string;
+ readonly code: string;
+ readonly name: string;
+ readonly nativeName: string;
+ readonly isRightToLeft: boolean;
+}
+
+export interface CreateLanguageRequest {
+ code: string;
+ name: string;
+ nativeName: string;
+ isRightToLeft: boolean;
+}
+
+export interface UpdateLanguageRequest extends CreateLanguageRequest {
+ isActive: boolean;
+}
+
+export type LanguageModalMode = 'create' | 'edit';
diff --git a/src/app/features/global-masters/languages/pages/language-list/language-list.html b/src/app/features/global-masters/languages/pages/language-list/language-list.html
index 1b547826..6aaa3e77 100644
--- a/src/app/features/global-masters/languages/pages/language-list/language-list.html
+++ b/src/app/features/global-masters/languages/pages/language-list/language-list.html
@@ -1,24 +1,8 @@
-
+
{{ value }}
@@ -27,97 +11,57 @@
-
+
-
+
@if (modalLoading()) {
-
-
- Loading language...
-
+
+
+ Loading language...
+
} @else {
-