-
-
-
-
-
-
-
-
-
- @if (getCurrencyFieldError('name', {
- required: 'Currency Name is required.',
- maxlength: 'Currency Name cannot exceed 100 characters.'
- })) {
-
- {{ getCurrencyFieldError('name', {
- required: 'Currency Name is required.',
- maxlength: 'Currency Name cannot exceed 100 characters.'
- }) }}
-
- }
+
+
+
-
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 ef0819e5..33263f1d 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,
@@ -29,11 +37,13 @@ import { DataTable } from '../../../../../shared/components/data-table/data-tabl
import { DataTableCellDirective } from '../../../../../shared/directives/data-table-cell.directive';
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;
@@ -47,7 +57,7 @@ interface CurrencyTableRow extends DataTableRecord {
@Component({
selector: 'currency-list',
standalone: true,
- imports: [DataTable, DataTableCellDirective, Modal, ReactiveFormsModule, ConfirmDialog],
+ imports: [DataTable, DataTableCellDirective, Modal, ReactiveFormsModule, FormInput, ConfirmDialog, CdkOverlayOrigin, CdkConnectedOverlay],
templateUrl: './currency-list.html',
styleUrl: './currency-list.scss',
})
@@ -73,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: [
@@ -143,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 },
@@ -481,7 +503,7 @@ export class CurrencyList {
queueMicrotask(() => {
const firstInvalidControl =
this.elementRef.nativeElement.querySelector(
- 'modal .custom-floating-input.is-invalid, modal [aria-invalid="true"]'
+ 'modal [data-form-control][aria-invalid="true"]'
);
firstInvalidControl?.focus();
@@ -496,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,
@@ -507,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`
@@ -541,6 +642,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/core/end-points/language/language.endpoints.ts b/src/app/features/global-masters/languages/data-access/language.endpoints.ts
similarity index 86%
rename from src/app/core/end-points/language/language.endpoints.ts
rename to src/app/features/global-masters/languages/data-access/language.endpoints.ts
index 3e591822..123ad1fa 100644
--- a/src/app/core/end-points/language/language.endpoints.ts
+++ b/src/app/features/global-masters/languages/data-access/language.endpoints.ts
@@ -1,4 +1,4 @@
-import { buildApiUrl } from '../../config/api-url.util';
+import { buildApiUrl } from '../../../../core/config/api-url.util';
export const LANGUAGE_ENDPOINTS = {
dataTable: buildApiUrl('masterAdmin', '/v1/languages/datatable'),
diff --git a/src/app/core/services/language/language.service.ts b/src/app/features/global-masters/languages/data-access/language.service.ts
similarity index 87%
rename from src/app/core/services/language/language.service.ts
rename to src/app/features/global-masters/languages/data-access/language.service.ts
index c5aed5d8..d491b95f 100644
--- a/src/app/core/services/language/language.service.ts
+++ b/src/app/features/global-masters/languages/data-access/language.service.ts
@@ -2,17 +2,17 @@ import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
-import { LANGUAGE_ENDPOINTS } from '../../end-points/language/language.endpoints';
+import { LANGUAGE_ENDPOINTS } from './language.endpoints';
import {
CreateLanguageRequest,
LanguageDto,
LanguageLookupDto,
UpdateLanguageRequest
-} from '../../models/language/language.model';
+} from '../models/language.model';
import {
DataTableQuery,
DataTableResult
-} from '../../../shared/components/data-table/data-table.types';
+} from '../../../../shared/components/data-table/data-table.types';
@Injectable({ providedIn: 'root' })
export class LanguageService {
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 {
-