changes related to session expire warning, session time out issue, exchange rate search label and modal resize-able

This commit is contained in:
Gagan7900
2026-08-17 14:06:54 +07:00
parent 17c6292521
commit adadfc20db
15 changed files with 53 additions and 126 deletions
@@ -4,7 +4,6 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { merge, fromEvent, Subscription } from 'rxjs'; import { merge, fromEvent, Subscription } from 'rxjs';
import { throttleTime } from 'rxjs/operators'; import { throttleTime } from 'rxjs/operators';
import { environment } from '../../../../environments/environment';
import { AuthService } from '../../../features/authentication/data-access/auth.service'; import { AuthService } from '../../../features/authentication/data-access/auth.service';
import { TokenStorageService } from './token-storage.service'; import { TokenStorageService } from './token-storage.service';
@@ -19,9 +18,6 @@ export class SessionTimeoutService implements OnDestroy {
readonly showWarning = signal(false); readonly showWarning = signal(false);
readonly remainingSeconds = signal(0); readonly remainingSeconds = signal(0);
private readonly configWarningAfterMs = environment.sessionTimeout?.warningAfterMs ?? 25 * 60 * 1000;
private readonly configLogoutAfterMs = environment.sessionTimeout?.logoutAfterMs ?? 30 * 60 * 1000;
private activitySubscription: Subscription | null = null; private activitySubscription: Subscription | null = null;
private warningTimer: ReturnType<typeof setTimeout> | null = null; private warningTimer: ReturnType<typeof setTimeout> | null = null;
private logoutTimer: ReturnType<typeof setTimeout> | null = null; private logoutTimer: ReturnType<typeof setTimeout> | null = null;
@@ -109,8 +105,15 @@ export class SessionTimeoutService implements OnDestroy {
this.showWarning.set(false); this.showWarning.set(false);
this.remainingSeconds.set(0); this.remainingSeconds.set(0);
// Calculate effective delays based on token expiry if available // Delays are derived solely from the access token expiry issued by the backend.
const { warningDelay, logoutDelay } = this.calculateEffectiveDelays(); const delays = this.calculateEffectiveDelays();
if (!delays) {
// No token expiry info available - nothing to schedule client-side.
return;
}
const { warningDelay, logoutDelay } = delays;
const safeWarningDelay = Math.max(Math.min(warningDelay, logoutDelay), 0); const safeWarningDelay = Math.max(Math.min(warningDelay, logoutDelay), 0);
const safeLogoutDelay = Math.max(logoutDelay, 0); const safeLogoutDelay = Math.max(logoutDelay, 0);
@@ -130,19 +133,14 @@ export class SessionTimeoutService implements OnDestroy {
} }
/** /**
* Calculate effective warning/logout delays based on the sooner of: * Derive warning/logout delays purely from the access token's expiry
* - Configured timeout values * timestamp issued by the backend - no client-side hard-coded durations.
* - Actual token expiry from backend
*/ */
private calculateEffectiveDelays(): { warningDelay: number; logoutDelay: number } { private calculateEffectiveDelays(): { warningDelay: number; logoutDelay: number } | null {
const accessTokenExpiresOn = this.tokenStorage.getAccessTokenExpiresOn(); const accessTokenExpiresOn = this.tokenStorage.getAccessTokenExpiresOn();
if (!accessTokenExpiresOn) { if (!accessTokenExpiresOn) {
// No token expiry info - use configured values return null;
return {
warningDelay: this.configWarningAfterMs,
logoutDelay: this.configLogoutAfterMs
};
} }
const tokenExpiryMs = Date.parse(accessTokenExpiresOn); const tokenExpiryMs = Date.parse(accessTokenExpiresOn);
@@ -155,14 +153,10 @@ export class SessionTimeoutService implements OnDestroy {
const tokenTimeRemaining = tokenExpiryMs - now; const tokenTimeRemaining = tokenExpiryMs - now;
// Use the sooner of configured timeout or token expiry // Warn at 80% of the remaining token lifetime.
// Warning at 80% of token lifetime or configured warning, whichever is sooner
const tokenBasedWarning = Math.floor(tokenTimeRemaining * 0.8);
const tokenBasedLogout = tokenTimeRemaining;
return { return {
warningDelay: Math.min(this.configWarningAfterMs, tokenBasedWarning), warningDelay: Math.floor(tokenTimeRemaining * 0.8),
logoutDelay: Math.min(this.configLogoutAfterMs, tokenBasedLogout) logoutDelay: tokenTimeRemaining
}; };
} }
@@ -2,9 +2,9 @@
[totalRecords]="tableStore.filteredRecords()" [pageIndex]="tableStore.queryState.pageIndex()" [totalRecords]="tableStore.filteredRecords()" [pageIndex]="tableStore.queryState.pageIndex()"
[pageSize]="tableStore.queryState.pageSize()" [pageSize]="tableStore.queryState.pageSize()"
[initialSortColumn]="tableStore.queryState.sortColumn()" [initialSortDirection]="tableStore.queryState.sortDirection()" [initialSortColumn]="tableStore.queryState.sortColumn()" [initialSortDirection]="tableStore.queryState.sortDirection()"
tableTitle="Exchange Rates (ROE)" buttonTitle="Add" tableTitle="Exchange Rates" buttonTitle="Add"
[showSearch]="true" [showAddButton]="true" [showFilterButton]="true" [filterActive]="showFilters()" [showSearch]="true" [showAddButton]="true" [showFilterButton]="true" [filterActive]="showFilters()"
searchPlaceholder="Search pair..." [searchDebounceTime]="300" toolTip="Add New Rate" (addClicked)="onAddRate()" searchPlaceholder="Search rates..." [searchDebounceTime]="300" toolTip="Add New Rate" (addClicked)="onAddRate()"
(searchChanged)="tableStore.onSearch($event)" (pageChanged)="tableStore.onPageChange($event)" (searchChanged)="tableStore.onSearch($event)" (pageChanged)="tableStore.onPageChange($event)"
(sortChanged)="tableStore.onSortChange($event)" (actionClicked)="onActionClick($event)" (sortChanged)="tableStore.onSortChange($event)" (actionClicked)="onActionClick($event)"
(filterClicked)="onToggleFilters()"> (filterClicked)="onToggleFilters()">
@@ -1,62 +1,3 @@
<<<<<<< HEAD
<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"
[showAddButton]="true"
[showFilterButton]="true"
[filterActive]="showFilters()"
searchPlaceholder="Search states..."
[searchDebounceTime]="300"
toolTip="Add State"
(addClicked)="onAddState()"
(filterClicked)="onToggleFilters()"
(searchChanged)="tableStore.onSearch($event)"
(pageChanged)="tableStore.onPageChange($event)"
(sortChanged)="tableStore.onSortChange($event)"
(actionClicked)="onActionClick($event)"
>
<ng-template appDataTableToolbar>
<form [formGroup]="countryFilterForm" (ngSubmit)="onApplyFilter($event)" autocomplete="off" class="flex flex-nowrap items-end gap-3">
<div class="w-[220px] shrink-0">
<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)="onFilterCountrySelected($event)"
/>
</div>
</form>
</ng-template>
<ng-template appDataTableFilterActions>
<button type="button" class="ti-btn ti-btn-primary-full !rounded-full !m-0 flex min-h-8 shrink-0 items-center justify-center gap-1.5 whitespace-nowrap !px-3 !py-1.5 !text-[0.75rem]" (click)="onApplyFilter($event)">
<span>Apply</span>
</button>
<button type="button" class="ti-btn ti-btn-light !rounded-full !m-0 flex min-h-8 shrink-0 items-center justify-center gap-1.5 whitespace-nowrap !px-3 !py-1.5 !text-[0.75rem]" (click)="onResetFilter()">
<span>Reset</span>
</button>
</ng-template>
</app-data-table>
=======
<app-data-table [columns]="columns()" [rows]="tableStore.rows()" [actions]="actions()" <app-data-table [columns]="columns()" [rows]="tableStore.rows()" [actions]="actions()"
[totalRecords]="tableStore.totalRecords()" [pageIndex]="tableStore.queryState.pageIndex()" [totalRecords]="tableStore.totalRecords()" [pageIndex]="tableStore.queryState.pageIndex()"
[pageSize]="tableStore.queryState.pageSize()" [pageSize]="tableStore.queryState.pageSize()"
@@ -88,7 +29,6 @@
<app-confirm-dialog title="Delete State" text="Do you really want to delete this state?" confirmButtonText="Delete" <app-confirm-dialog title="Delete State" text="Do you really want to delete this state?" confirmButtonText="Delete"
cancelButtonText="Cancel" (confirmed)="onDeleteConfirmed()" (cancelled)="onDeleteCancelled()" /> cancelButtonText="Cancel" (confirmed)="onDeleteConfirmed()" (cancelled)="onDeleteCancelled()" />
>>>>>>> dev
<app-state-form-modal [open]="tableStore.showModal()" [mode]="tableStore.modalMode()" <app-state-form-modal [open]="tableStore.showModal()" [mode]="tableStore.modalMode()"
[stateId]="tableStore.selectedItem()?.id ?? null" (saved)="tableStore.refresh(); tableStore.closeModal()" [stateId]="tableStore.selectedItem()?.id ?? null" (saved)="tableStore.refresh(); tableStore.closeModal()"
@@ -5,8 +5,8 @@ import { Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, finalize, switchMap } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, finalize, switchMap } from 'rxjs/operators';
import { NotificationService } from '../../../../../core/services/common/notification.service'; 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 { StateDto } from '../../models/state.model';
import { StateService } from '../../data-access/state.service'; import { StateService } from '../../data-access/state.service';
import { DataTable, DataTableToolbarDirective } from '../../../../../shared/components/data-table/data-table'; import { DataTable, DataTableToolbarDirective } from '../../../../../shared/components/data-table/data-table';
import { DataTableStore } from '../../../../../shared/components/data-table/data-table.store'; import { DataTableStore } from '../../../../../shared/components/data-table/data-table.store';
@@ -133,9 +133,6 @@ export class StateList implements OnInit {
this.showFilters.update(value => !value); this.showFilters.update(value => !value);
} }
onFilterCountrySelected(country: CountryLookupDto | null): void {
this.selectedCountryLookup.set(country);
this.countryFilterForm.controls.countryId.setValue(country ? country.id : '');
onCountrySearchChanged(term: string): void { onCountrySearchChanged(term: string): void {
this.countrySearch$.next(term); this.countrySearch$.next(term);
} }
@@ -434,7 +434,7 @@
cursor: default; cursor: default;
} }
:host-context(.dark) .data-table-modern .ti-pagination li .page-link { :host-context(.dark) .data-table-modern .ti-pagination li .page-link,
:host-context(.dark) .data-table-modern ::ng-deep .p-paginator .p-paginator-page, :host-context(.dark) .data-table-modern ::ng-deep .p-paginator .p-paginator-page,
:host-context(.dark) .data-table-modern ::ng-deep .p-paginator .p-paginator-prev, :host-context(.dark) .data-table-modern ::ng-deep .p-paginator .p-paginator-prev,
:host-context(.dark) .data-table-modern ::ng-deep .p-paginator .p-paginator-next { :host-context(.dark) .data-table-modern ::ng-deep .p-paginator .p-paginator-next {
@@ -18,7 +18,7 @@
} }
::ng-deep .form-control:focus { ::ng-deep .form-control:focus {
border-color: var(--color-primary); border-color: var(--color-primary) !important;
box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--color-primary) 20%, transparent); box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--color-primary) 20%, transparent);
} }
@@ -308,8 +308,9 @@
.shared-form-input::placeholder { color: transparent; } .shared-form-input::placeholder { color: transparent; }
.shared-form-control.is-focused .shared-form-input, .shared-form-control.is-focused .shared-form-input,
.shared-form-control.has-value .shared-form-input { .shared-form-control.has-value .shared-form-input,
border-color: var(--color-primary); .shared-form-input:focus {
border-color: var(--color-primary) !important;
} }
.shared-floating-label { .shared-floating-label {
+1 -1
View File
@@ -74,7 +74,7 @@
tabindex="-1" tabindex="-1"
(mousedown)="$event.stopPropagation()"> (mousedown)="$event.stopPropagation()">
<div class="modern-modal"> <div class="modern-modal" [style.width]="modalInitialWidth()">
@if (showHeader()) { @if (showHeader()) {
<div class="modern-modal-header"> <div class="modern-modal-header">
+12 -2
View File
@@ -91,7 +91,7 @@
position: relative; position: relative;
width: 100%; max-width: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -113,10 +113,18 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; min-width: 320px;
min-height: 220px;
max-width: 96vw;
max-height: 90vh; max-height: 90vh;
box-sizing: border-box;
overflow: hidden; overflow: hidden;
resize: both;
&::-webkit-resizer {
background: transparent;
}
border-radius: 20px; border-radius: 20px;
@@ -920,6 +928,8 @@
border-radius: 18px; border-radius: 18px;
max-height: 95vh; max-height: 95vh;
width: 100% !important;
resize: none;
} }
.modal-header-content { .modal-header-content {
+9 -10
View File
@@ -40,16 +40,16 @@ export class Modal implements OnDestroy {
readonly closed = output<void>(); readonly closed = output<void>();
readonly submitted = output<void>(); readonly submitted = output<void>();
readonly modalSizeClass = computed(() => { readonly modalInitialWidth = computed(() => {
const sizes: Record<ModalSize, string> = { const widths: Record<ModalSize, string> = {
sm: 'max-w-md', sm: '28rem',
md: 'max-w-2xl', md: '42rem',
lg: 'max-w-4xl', lg: '56rem',
xl: 'max-w-6xl', xl: '72rem',
full: 'max-w-[96vw]' full: '96vw'
}; };
return sizes[this.size()]; return widths[this.size()];
}); });
readonly modalBoxClass = computed(() => { readonly modalBoxClass = computed(() => {
@@ -60,8 +60,7 @@ export class Modal implements OnDestroy {
'ease-out', 'ease-out',
'relative', 'relative',
'z-[1]', 'z-[1]',
'pointer-events-auto', 'pointer-events-auto'
this.modalSizeClass()
].join(' '); ].join(' ');
}); });
-6
View File
@@ -1,13 +1,7 @@
export interface AppSessionTimeoutConfig {
warningAfterMs: number;
logoutAfterMs: number;
}
export interface AppEnvironment { export interface AppEnvironment {
production: boolean; production: boolean;
api: { api: {
identity: string; identity: string;
masterAdmin: string; masterAdmin: string;
}; };
sessionTimeout: AppSessionTimeoutConfig;
} }
-4
View File
@@ -6,8 +6,4 @@ export const environment: AppEnvironment = {
identity: '/api', identity: '/api',
masterAdmin: '/api', masterAdmin: '/api',
}, },
sessionTimeout: {
warningAfterMs: 25 * 60 * 1000,
logoutAfterMs: 30 * 60 * 1000,
},
}; };
-4
View File
@@ -12,10 +12,6 @@ export const environment: AppEnvironment = {
// identity: 'https://identity.yourdomain.com/api', // identity: 'https://identity.yourdomain.com/api',
// masterAdmin: 'https://master-admin.yourdomain.com/api', // masterAdmin: 'https://master-admin.yourdomain.com/api',
}, },
sessionTimeout: {
warningAfterMs: 25 * 60 * 1000,
logoutAfterMs: 30 * 60 * 1000,
},
}; };
/* /*
+2
View File
@@ -11,6 +11,8 @@
<link rel="preconnect" href="https://fonts.gstatic.com"> <link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
</head> </head>
+1 -3
View File
@@ -2,9 +2,7 @@
@forward "../public/assets/css/style.css"; @forward "../public/assets/css/style.css";
@forward "../node_modules/ngx-toastr/toastr.css"; @forward "../node_modules/ngx-toastr/toastr.css";
@import "flatpickr/dist/flatpickr.css"; @use "flatpickr/dist/flatpickr.css";
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@500;600&display=swap');
/* Etihad Altis Text (local font) */ /* Etihad Altis Text (local font) */
@font-face { @font-face {