diff --git a/Common-UI b/Common-UI index 93c9157..e9304ae 160000 --- a/Common-UI +++ b/Common-UI @@ -1 +1 @@ -Subproject commit 93c915707165d8ebeefa5c62cc087b08184b261a +Subproject commit e9304ae4c85709a82608e6f900429a857fc703ba diff --git a/pom.xml b/pom.xml index 6fc0be4..042ca81 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.iemr.tm-ui tm-ui - 3.6.0 + 3.6.1 TM-UI Piramal - tm: Module ui war diff --git a/src/app/app-modules/core/services/confirmation.service.ts b/src/app/app-modules/core/services/confirmation.service.ts index 761dc63..456246f 100644 --- a/src/app/app-modules/core/services/confirmation.service.ts +++ b/src/app/app-modules/core/services/confirmation.service.ts @@ -11,7 +11,7 @@ import { CommonDialogComponent } from '../components/common-dialog/common-dialog @Injectable() export class ConfirmationService { constructor( - private dialog: MatDialog, + public dialog: MatDialog, @Inject(DOCUMENT) doc: any, ) {} diff --git a/src/app/app-modules/core/services/http-interceptor.service.ts b/src/app/app-modules/core/services/http-interceptor.service.ts index 60b391e..97cfbb6 100644 --- a/src/app/app-modules/core/services/http-interceptor.service.ts +++ b/src/app/app-modules/core/services/http-interceptor.service.ts @@ -9,170 +9,335 @@ import { HttpErrorResponse, HttpHeaders, } from '@angular/common/http'; -import { catchError, tap } from 'rxjs/operators'; +import { catchError, tap, finalize } from 'rxjs/operators'; import { Observable, of } from 'rxjs'; import { Router } from '@angular/router'; import { throwError } from 'rxjs/internal/observable/throwError'; +import { MatDialog } from '@angular/material/dialog'; import { SpinnerService } from './spinner.service'; import { ConfirmationService } from './confirmation.service'; import { environment } from 'src/environments/environment'; import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +/** + * HTTP Interceptor Service + * Handles: + * - Request authorization headers + * - Response success/error handling + * - Session management and timeout + * - Loading spinner management + * - User feedback on errors + */ @Injectable({ providedIn: 'root', }) export class HttpInterceptorService implements HttpInterceptor { - timerRef: any; - currentLanguageSet: any; - donotShowSpinnerUrl = [ + private sessionTimeoutRef: any; + private currentLanguageSet: any; + private readonly SESSION_TIMEOUT_DURATION = 27 * 60 * 1000; // 27 minutes + private readonly SESSION_WARNING_TIME = 1.5 * 60 * 1000; // 1.5 minutes before timeout + private readonly EXCLUDED_SPINNER_URLS = [ environment.syncDownloadProgressUrl, environment.ioturl, ]; + constructor( private spinnerService: SpinnerService, private router: Router, private confirmationService: ConfirmationService, private http: HttpClient, - readonly sessionstorage: SessionStorageService, + readonly sessionStorage: SessionStorageService, + private matDialog: MatDialog, ) {} intercept( req: HttpRequest, next: HttpHandler, ): Observable> { - const key: any = sessionStorage.getItem('key'); - let modifiedReq = req; - const isPlatformFeedback = - req.url && req.url.toLowerCase().includes('/platform-feedback'); + // Add authorization header + const modifiedReq = this.addAuthorizationHeader(req); - if (isPlatformFeedback) { - // For platform-feedback: remove Authorization and force JSON content-type - const headers = req.headers - .delete('Authorization') - .set('Content-Type', 'application/json'); - modifiedReq = req.clone({ headers }); - } else { - if (req.body instanceof FormData) { - modifiedReq = req.clone({ - headers: req.headers.set('Authorization', key || ''), - }); - } else { - if (key !== undefined && key !== null) { - modifiedReq = req.clone({ - headers: req.headers - .set('Authorization', key) - .set('Content-Type', 'application/json'), - }); - } else { - modifiedReq = req.clone({ - headers: req.headers.set('Authorization', ''), - }); - } - } + // Show spinner unless URL is in exclusion list + const shouldShowSpinner = !this.isExcludedUrl(req.url); + if (shouldShowSpinner) { + this.spinnerService.setLoading(true); } + return next.handle(modifiedReq).pipe( - tap((event: HttpEvent) => { - if (req.url !== undefined && !req.url.includes('cti/getAgentState')) - this.spinnerService.setLoading(true); - if (event instanceof HttpResponse) { - console.log(event.body); - this.onSuccess(req.url, event.body); + tap((event: HttpEvent) => this.handleResponse(event)), + catchError((error: HttpErrorResponse) => + this.handleError(error, req.url), + ), + finalize(() => { + if (shouldShowSpinner) { this.spinnerService.setLoading(false); - return event.body; - } - }), - catchError((error: HttpErrorResponse) => { - console.error(error); - this.spinnerService.setLoading(false); - - if(error.status === 401){ - this.sessionstorage.clear(); - this.confirmationService.alert(this.currentLanguageSet.sessionExpiredPleaseLogin, 'error'); - setTimeout(() => this.router.navigate(['/login']), 0); - } else if (error.status === 403) { - this.confirmationService.alert( - this.currentLanguageSet.accessDenied, - 'error', - ); - } else if (error.status === 500) { - this.confirmationService.alert( - this.currentLanguageSet.internaleServerError, - 'error', - ); - } else { - this.confirmationService.alert( - error.message || this.currentLanguageSet.somethingWentWrong, - 'error', - ); } - return throwError(error.error); }), ); } + /** + * Adds authorization header to request + * Special handling for platform-feedback endpoint (no auth required) + */ + private addAuthorizationHeader(req: HttpRequest): HttpRequest { + const authToken = sessionStorage.getItem('key'); + const isPlatformFeedback = req.url + ?.toLowerCase() + .includes('/platform-feedback'); - private onSuccess(url: string, response: any): void { - if (this.timerRef) clearTimeout(this.timerRef); + if (isPlatformFeedback) { + // Remove Authorization for feedback endpoint + return req.clone({ + headers: req.headers + .delete('Authorization') + .set('Content-Type', 'application/json'), + }); + } - if ( - response.statusCode === 5002 && - url.indexOf('user/userAuthenticate') < 0 - ) { - sessionStorage.clear(); - this.sessionstorage.clear(); - setTimeout(() => this.router.navigate(['/login']), 0); - this.confirmationService.alert(response.errorMessage, 'error'); + let headers = req.headers; + + if (req.body instanceof FormData) { + headers = headers.set('Authorization', authToken || ''); } else { - this.startTimer(); + headers = headers + .set('Authorization', authToken || '') + .set('Content-Type', 'application/json'); + } + + return req.clone({ headers }); + } + + /** + * Check if URL is excluded from spinner display + */ + private isExcludedUrl(url: string): boolean { + return ( + url === undefined || + url.includes('cti/getAgentState') || + this.EXCLUDED_SPINNER_URLS.some((excludedUrl) => url.includes(excludedUrl)) + ); + } + + /** + * Handle successful HTTP responses + * Checks for application-level errors in response body + */ + private handleResponse(event: HttpEvent): void { + if (event instanceof HttpResponse) { + const response = event.body; + + // Check for application-level errors (statusCode in body) + if ( + response && + typeof response === 'object' && + response.statusCode === 5002 && + !event.url?.includes('user/userAuthenticate') + ) { + this.handleSessionExpired(); + return; + } + + // Reset session timeout on successful response + this.resetSessionTimeout(); + } + } + + /** + * Handle HTTP errors with appropriate user-facing messages + */ + private handleError( + error: HttpErrorResponse, + url: string, + ): Observable { + const errorMessage = this.getErrorMessage(error); + + // Close all open dialogs before showing error + this.matDialog.closeAll(); + + // Show error based on status code + switch (error.status) { + case 401: + this.handleUnauthorized(); + break; + case 403: + this.handleForbidden(); + break; + case 404: + this.handleNotFound(errorMessage); + break; + case 500: + case 502: + case 503: + case 504: + this.handleServerError(error.status, errorMessage); + break; + default: + this.handleGenericError(errorMessage); + } + + return throwError(() => error); + } + + /** + * Extract error message from HTTP response + * Prioritizes server-provided messages over generic ones + */ + private getErrorMessage(error: HttpErrorResponse): string { + // Try to get message from error response body + if (error.error) { + if (typeof error.error === 'string') { + return error.error; + } + if ( + error.error.message && + typeof error.error.message === 'string' + ) { + return error.error.message; + } + if (error.error.error && typeof error.error.error === 'string') { + return error.error.error; + } } + + // Fallback to HTTP status text + return error.statusText || 'Unknown error occurred'; + } + + /** + * Handle 401 Unauthorized - Session expired + */ + private handleUnauthorized(): void { + this.confirmationService.alert( + this.currentLanguageSet?.sessionExpiredPleaseLogin || + 'Session has expired, please login again.', + 'error', + ); + this.handleSessionExpired(); + } + + /** + * Handle 403 Forbidden - Access denied + */ + private handleForbidden(): void { + this.confirmationService.alert( + this.currentLanguageSet?.accessDenied || + 'Access Denied. You do not have permission to access this resource.', + 'error', + ); + this.handleSessionExpired(); } - startTimer() { - this.timerRef = setTimeout( - () => { - console.log('there', Date()); - - if ( - sessionStorage.getItem('authenticationToken') && - sessionStorage.getItem('isAuthenticated') - ) { - this.confirmationService - .alert( - 'Your session is about to Expire. Do you need more time ? ', - 'sessionTimeOut', - ) - .afterClosed() - .subscribe((result: any) => { - if (result.action === 'continue') { - this.http.post(environment.extendSessionUrl, {}).subscribe( - (res: any) => {}, - (err: any) => {}, - ); - } else if (result.action === 'timeout') { - clearTimeout(this.timerRef); - sessionStorage.clear(); - this.sessionstorage.clear(); - this.confirmationService.alert( - this.currentLanguageSet.sessionExpired, - 'error', - ); - this.router.navigate(['/login']); - } else if (result.action === 'cancel') { - setTimeout(() => { - clearTimeout(this.timerRef); - sessionStorage.clear(); - this.sessionstorage.clear(); - this.confirmationService.alert( - this.currentLanguageSet.sessionExpired, - 'error', - ); - this.router.navigate(['/login']); - }, result.remainingTime * 1000); - } - }); + /** + * Handle 404 Not Found + */ + private handleNotFound(errorMessage: string): void { + this.confirmationService.alert( + this.currentLanguageSet?.notFound || `Resource not found: ${errorMessage}`, + 'error', + ); + } + + /** + * Handle 5xx Server Errors + */ + private handleServerError(status: number, errorMessage: string): void { + const message = + this.currentLanguageSet?.internaleServerError || + `Server error (${status}): ${errorMessage}`; + + this.confirmationService.alert(message, 'error'); + } + + /** + * Handle generic/unknown errors + */ + private handleGenericError(errorMessage: string): void { + const message = + this.currentLanguageSet?.somethingWentWrong || + `Something went wrong: ${errorMessage}`; + + this.confirmationService.alert(message, 'error'); + } + + /** + * Handle session expiration - clear storage and redirect + */ + private handleSessionExpired(): void { + this.clearSessionTimeout(); + sessionStorage.clear(); + this.sessionStorage.clear(); + this.router.navigate(['/login']); + } + + /** + * Reset session timeout on user activity + */ + private resetSessionTimeout(): void { + this.clearSessionTimeout(); + this.startSessionTimeout(); + } + + /** + * Start session timeout timer + * Warns user 1.5 minutes before actual timeout (27 minutes total) + */ + private startSessionTimeout(): void { + this.sessionTimeoutRef = setTimeout(() => { + const isAuthenticated = + sessionStorage.getItem('authenticationToken') && + sessionStorage.getItem('isAuthenticated'); + + if (isAuthenticated) { + this.showSessionTimeoutWarning(); + } + }, this.SESSION_TIMEOUT_DURATION); + } + + /** + * Show session timeout warning dialog + */ + private showSessionTimeoutWarning(): void { + this.confirmationService + .alert( + 'Your session is about to expire. Do you need more time?', + 'sessionTimeOut', + ) + .afterClosed() + .subscribe((result: any) => { + if (!result) return; + + if (result.action === 'continue') { + this.extendSession(); + } else if (result.action === 'timeout' || result.action === 'cancel') { + this.handleSessionExpired(); } + }); + } + + /** + * Extend user session by calling backend endpoint + */ + private extendSession(): void { + this.http.post(environment.extendSessionUrl, {}).subscribe( + (res: any) => { + // Session extended successfully, restart timeout + this.resetSessionTimeout(); + }, + (err: any) => { + // Silently fail - let normal error handling take over + console.warn('Failed to extend session', err); }, - 27 * 60 * 1000, ); } + + /** + * Clear session timeout + */ + private clearSessionTimeout(): void { + if (this.sessionTimeoutRef) { + clearTimeout(this.sessionTimeoutRef); + this.sessionTimeoutRef = null; + } + } } diff --git a/src/app/app-modules/nurse-doctor/anc/anc.component.ts b/src/app/app-modules/nurse-doctor/anc/anc.component.ts index 17d68ec..82e3ed3 100644 --- a/src/app/app-modules/nurse-doctor/anc/anc.component.ts +++ b/src/app/app-modules/nurse-doctor/anc/anc.component.ts @@ -134,6 +134,46 @@ export class AncComponent implements OnInit, OnChanges, OnDestroy, DoCheck { visitCode: this.sessionstorage.getItem('visitCode'), }; + const immunizationForm = patientANCForm.get('patientANCImmunizationForm'); + + if (immunizationForm) { + const ttDateFields = [ + 'dateReceivedForTT_1', + 'dateReceivedForTT_2', + 'dateReceivedForTT_3', + ]; + + ttDateFields.forEach((field) => { + const value = immunizationForm.get(field)?.value; + + if (value) { + immunizationForm.patchValue({ + [field]: this.normalizeToUTCMidnight(new Date(value)), + }); + } + }); + } + + const ancDetailsForm = patientANCForm.get('patientANCDetailsForm'); + + if (ancDetailsForm) { + const lmpDateValue = ancDetailsForm.get('lmpDate')?.value; + + if (lmpDateValue) { + ancDetailsForm.patchValue({ + lmpDate: this.normalizeToUTCMidnight(new Date(lmpDateValue)), + }); + } + + const expDelDtValue = ancDetailsForm.get('expDelDt')?.value; + + if (expDelDtValue) { + ancDetailsForm.patchValue({ + expDelDt: this.normalizeToUTCMidnight(new Date(expDelDtValue)), + }); + } + } + this.updateANCDetailsSubs = this.doctorService .updateANCDetails(patientANCForm, temp) .subscribe( @@ -152,6 +192,16 @@ export class AncComponent implements OnInit, OnChanges, OnDestroy, DoCheck { ); } + private normalizeToUTCMidnight(date: Date | null | undefined): string | null { + if (!date) return null; + + const d = new Date(date); + const utcDate = new Date( + Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0) + ); + return utcDate.toISOString(); + } + getHRPDetails() { const beneficiaryRegID = this.sessionstorage.getItem('beneficiaryRegID'); const visitCode = this.sessionstorage.getItem('visitCode'); diff --git a/src/app/app-modules/nurse-doctor/case-record/general-case-record/prescription/prescription.component.html b/src/app/app-modules/nurse-doctor/case-record/general-case-record/prescription/prescription.component.html index a018ddb..ecd338d 100644 --- a/src/app/app-modules/nurse-doctor/case-record/general-case-record/prescription/prescription.component.html +++ b/src/app/app-modules/nurse-doctor/case-record/general-case-record/prescription/prescription.component.html @@ -45,7 +45,6 @@ name="form" [(ngModel)]="currentPrescription.formName" (selectionChange)="getFormValueChanged()" - required > {{ item }} @@ -157,7 +152,6 @@ name="unit" [(ngModel)]="currentPrescription.unit" [disabled]="!currentPrescription.drugID" - required > 0) { + formArray.removeAt(0); + } + + for (let i = 0; i < temp.length; i++) { + formArray.push(this.initAllergyList()); + } + + this.allerySelectList = []; + this.previousSelectedAlleryList = []; + for (let i = 0; i < temp.length; i++) { const allergyType = this.allergyMasterData.filter((item) => { return item.allergyType === temp[i].allergyType; }); + if (allergyType.length > 0) temp[i].allergyType = allergyType[0]; - if (this.masterData.AllergicReactionTypes !== undefined) { - temp[i].typeOfAllergicReactions = - this.masterData.AllergicReactionTypes.filter((item: any) => { - let flag = false; - temp[i].typeOfAllergicReactions.forEach((element: any) => { - if (element.name === item.name) flag = true; - }); - return flag; + temp[i].typeOfAllergicReactions = + this.masterData.AllergicReactionTypes.filter((item: any) => { + let flag = false; + temp[i].typeOfAllergicReactions.forEach((element: any) => { + if (element.name === item.name) flag = true; }); - } + return flag; + }); if (temp[i].otherAllergicReaction) temp[i].enableOtherAllergy = true; + const selectedAllergies = temp + .filter((t: any, idx: any) => idx !== i && t.allergyType) + .map((t: any) => t.allergyType.allergyType); + + const availableAllergies = this.allergyMasterData.filter( + (item) => !selectedAllergies.includes(item.allergyType), + ); + + this.allerySelectList.push(availableAllergies.slice()); + if (temp[i].allergyType) { - const k: any = formArray.get('' + i); + this.previousSelectedAlleryList[i] = temp[i].allergyType; + } + + const k: any = formArray.get('' + i); + if (k) { k.patchValue(temp[i]); k.markAsTouched(); - k.markAsDirty(); - this.filterAlleryList(temp[i].allergyType, i); - if ( - k?.get('snomedTerm')?.value !== null && - k?.get('typeOfAllergicReactions')?.value !== null - ) { - k?.get('snomedTerm')?.enable(); - k?.get('typeOfAllergicReactions')?.enable(); + + if (temp[i].allergyType) { + k.get('snomedTerm')?.enable(); + k.get('typeOfAllergicReactions')?.enable(); } } - - if (i + 1 < temp.length) this.addAllergy(); } } } diff --git a/src/app/app-modules/nurse-doctor/nurse-worklist-wrapper/nurse-mmu-tm-referred-worklist/nurse-mmu-tm-referred-worklist.component.ts b/src/app/app-modules/nurse-doctor/nurse-worklist-wrapper/nurse-mmu-tm-referred-worklist/nurse-mmu-tm-referred-worklist.component.ts index e1e07a6..8346b6f 100644 --- a/src/app/app-modules/nurse-doctor/nurse-worklist-wrapper/nurse-mmu-tm-referred-worklist/nurse-mmu-tm-referred-worklist.component.ts +++ b/src/app/app-modules/nurse-doctor/nurse-worklist-wrapper/nurse-mmu-tm-referred-worklist/nurse-mmu-tm-referred-worklist.component.ts @@ -136,6 +136,10 @@ export class NurseMmuTmReferredWorklistComponent this.dataSource.paginator = this.paginator; }, (err: any) => { + if (err?.status == 401) { + // do nothing as http-interceptor will handle + return; + } this.confirmationService.alert(err, 'error'); }, ); diff --git a/src/app/app-modules/nurse-doctor/pnc/pnc.component.ts b/src/app/app-modules/nurse-doctor/pnc/pnc.component.ts index 13e14f6..49bed08 100644 --- a/src/app/app-modules/nurse-doctor/pnc/pnc.component.ts +++ b/src/app/app-modules/nurse-doctor/pnc/pnc.component.ts @@ -158,13 +158,25 @@ export class PncComponent implements OnInit, DoCheck, OnChanges, OnDestroy { })[0]; } - tempPNCData.dDate = new Date(tempPNCData.dateOfDelivery); + tempPNCData.dDate = this.normalizeToUTCMidnight( + new Date(tempPNCData.dateOfDelivery), + ); const patchPNCdata = Object.assign({}, tempPNCData); this.patientPNCForm.patchValue(tempPNCData); }); } + private normalizeToUTCMidnight(date: Date | null | undefined): string | null { + if (!date) return null; + + const d = new Date(date); + const utcDate = new Date( + Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0) + ); + return utcDate.toISOString(); + } + updatePatientPNC(patientPNCForm: any) { const temp = { beneficiaryRegID: this.sessionstorage.getItem('beneficiaryRegID'), @@ -174,6 +186,14 @@ export class PncComponent implements OnInit, DoCheck, OnChanges, OnDestroy { visitCode: this.sessionstorage.getItem('visitCode'), }; + const dDate = patientPNCForm.get('dDate')?.value; + if (dDate) { + patientPNCForm.patchValue({ + dateOfDelivery: this.normalizeToUTCMidnight(new Date(dDate)), + dDate: this.normalizeToUTCMidnight(new Date(dDate)), + }); + } + this.doctorService.updatePNCDetails(patientPNCForm, temp).subscribe( (res: any) => { if (res.statusCode === 200 && res.data !== null) { diff --git a/src/app/app-modules/nurse-doctor/quick-consult/quick-consult.component.html b/src/app/app-modules/nurse-doctor/quick-consult/quick-consult.component.html index 81a8c18..03a6483 100644 --- a/src/app/app-modules/nurse-doctor/quick-consult/quick-consult.component.html +++ b/src/app/app-modules/nurse-doctor/quick-consult/quick-consult.component.html @@ -786,7 +786,7 @@

name="form" [(ngModel)]="tempform" (selectionChange)="getFormValueChanged()" - required + > [(ngModel)]="tempDrugName" (keyup)="filterMedicine(tempDrugName)" (blur)="reEnterMedicine()" - required [matAutocomplete]="autoGroup" /> name="dose" [(ngModel)]="currentPrescription.dose" [disabled]="!currentPrescription.drugID" - required > name="frequency" [(ngModel)]="currentPrescription.frequency" [disabled]="!currentPrescription.drugID" - required > name="duration" [(ngModel)]="currentPrescription.duration" [disabled]="!currentPrescription.drugID" - required > name="unit" [(ngModel)]="currentPrescription.unit" [disabled]="!currentPrescription.drugID" - required > name="quantity" [(ngModel)]="currentPrescription.qtyPrescribed" [disabled]="!currentPrescription.drugID" - required >