+
diff --git a/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/pnc-diagnosis/pnc-diagnosis.component.ts b/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/pnc-diagnosis/pnc-diagnosis.component.ts
index e77880e..62e157e 100644
--- a/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/pnc-diagnosis/pnc-diagnosis.component.ts
+++ b/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/pnc-diagnosis/pnc-diagnosis.component.ts
@@ -182,13 +182,11 @@ export class PncDiagnosisComponent
);
}
- getConfirmatoryDiagnosisList(): AbstractControl[] | null {
- const confirmatoryDiagnosisListControl = this.generalDiagnosisForm.get(
- 'confirmatoryDiagnosisList',
+ get confirmatoryDiagnosisControls(): AbstractControl[] {
+ return (
+ (this.generalDiagnosisForm.get('confirmatoryDiagnosisList') as FormArray)
+ ?.controls || []
);
- return confirmatoryDiagnosisListControl instanceof FormArray
- ? confirmatoryDiagnosisListControl.controls
- : null;
}
beneficiaryDetailsSubscription: any;
@@ -423,34 +421,179 @@ export class PncDiagnosisComponent
}
}
- onDiagnosisInputKeyup(value: string, index: number) {
- if (value.length >= 3) {
- this.masterdataService
- .searchDiagnosisBasedOnPageNo(value, index)
- .subscribe((results: any) => {
- this.suggestedDiagnosisList[index] = results?.data?.sctMaster;
- });
- } else {
- this.suggestedDiagnosisList[index] = [];
- }
+ displayDiagnosis(diagnosis: any): string {
+ return typeof diagnosis === 'string' ? diagnosis : diagnosis?.term || '';
}
- displayDiagnosis(diagnosis: any): string {
+ displayConfirmatoryDiagnosis(diagnosis: any): string {
return typeof diagnosis === 'string' ? diagnosis : diagnosis?.term || '';
}
- onDiagnosisSelected(selected: any, index: number) {
- // this.patientQuickConsultForm.get(['provisionalDiagnosisList', index])?.setValue(selected);
- const diagnosisFormArray = this.generalDiagnosisForm.get(
- 'provisionalDiagnosisList'
- ) as FormArray;
- const diagnosisFormGroup = diagnosisFormArray.at(index) as FormGroup;
-
- // Set the nested and top-level fields
- diagnosisFormGroup.patchValue({
- viewProvisionalDiagnosisProvided: selected,
- conceptID: selected?.conceptID || null,
- term: selected?.term || null,
+ // --- Shared scroll state for both provisional + confirmatory ---
+ private readonly PAGE_BASE = 0;
+ private readonly BOOTSTRAP_MAX_PAGES = 3;
+
+ state: any = {
+ provisional: {
+ suggested: [] as any[][],
+ lastQueryByIndex: [] as string[],
+ pageByIndex: [] as number[],
+ loadingMore: [] as boolean[],
+ noMore: [] as boolean[],
+ wantMore: [] as boolean[],
+ },
+ confirmatory: {
+ suggested: [] as any[][],
+ lastQueryByIndex: [] as string[],
+ pageByIndex: [] as number[],
+ loadingMore: [] as boolean[],
+ noMore: [] as boolean[],
+ wantMore: [] as boolean[],
+ },
+ };
+
+ // --- Keyup handler (shared) ---
+ onDiagnosisInputKeyup(
+ type: 'provisional' | 'confirmatory',
+ value: string,
+ index: number,
+ ) {
+ const term = (value || '').trim();
+ const s = this.state[type];
+
+ if (term.length >= 3) {
+ if (s.lastQueryByIndex[index] !== term) {
+ s.lastQueryByIndex[index] = term;
+ s.pageByIndex[index] = 0;
+ s.noMore[index] = false;
+ s.wantMore[index] = false;
+ s.suggested[index] = [];
+ }
+ this.fetchPage(type, index, false);
+ } else {
+ s.lastQueryByIndex[index] = '';
+ s.pageByIndex[index] = 0;
+ s.noMore[index] = false;
+ s.wantMore[index] = false;
+ s.suggested[index] = [];
+ }
+ }
+
+ // --- When user picks an option ---
+ onDiagnosisSelected(
+ type: 'provisional' | 'confirmatory',
+ selected: any,
+ index: number,
+ ) {
+ const controlName =
+ type === 'provisional'
+ ? 'provisionalDiagnosisList'
+ : 'confirmatoryDiagnosisList';
+ const formArray = this.generalDiagnosisForm.get(controlName) as FormArray;
+ const fg = formArray.at(index) as FormGroup;
+
+ fg.patchValue({
+ [type === 'provisional'
+ ? 'viewProvisionalDiagnosisProvided'
+ : 'confirmatoryDiagnosis']: selected,
+ conceptID: selected?.conceptID ?? null,
+ term: selected?.term ?? null,
});
}
+
+ // --- Autocomplete scroll hooks ---
+ onPanelReady(
+ type: 'provisional' | 'confirmatory',
+ index: number,
+ panelEl: HTMLElement,
+ ) {
+ const s = this.state[type];
+ if (panelEl.scrollHeight <= panelEl.clientHeight && !s.noMore[index]) {
+ this.bootstrapUntilScrollable(type, index, panelEl);
+ }
+ }
+
+ onAutoNearEnd(type: 'provisional' | 'confirmatory', index: number) {
+ const s = this.state[type];
+ if (!s.loadingMore[index] && !s.noMore[index]) {
+ this.fetchPage(type, index, true);
+ } else if (s.loadingMore[index]) {
+ s.wantMore[index] = true;
+ }
+ }
+
+ private bootstrapUntilScrollable(
+ type: 'provisional' | 'confirmatory',
+ rowIndex: number,
+ panelEl: HTMLElement,
+ ) {
+ const s = this.state[type];
+ let fetched = 0;
+ const tryFill = () => {
+ const scrollable = panelEl.scrollHeight > panelEl.clientHeight;
+ if (
+ scrollable ||
+ s.noMore[rowIndex] ||
+ fetched >= this.BOOTSTRAP_MAX_PAGES
+ )
+ return;
+ if (s.loadingMore[rowIndex]) {
+ requestAnimationFrame(tryFill);
+ return;
+ }
+ fetched++;
+ this.fetchPage(type, rowIndex, true);
+ requestAnimationFrame(tryFill);
+ };
+ if (s.lastQueryByIndex[rowIndex]?.length >= 3) tryFill();
+ }
+
+ private fetchPage(
+ type: 'provisional' | 'confirmatory',
+ index: number,
+ append = false,
+ ) {
+ const s = this.state[type];
+ const term = s.lastQueryByIndex[index];
+ if (!term) return;
+
+ const nextLogical = (s.pageByIndex[index] ?? 0) + (append ? 1 : 0);
+ const pageAtReq = nextLogical + this.PAGE_BASE;
+ if (s.loadingMore[index]) return;
+ s.loadingMore[index] = true;
+
+ this.masterdataService
+ .searchDiagnosisBasedOnPageNo(term, pageAtReq)
+ .subscribe({
+ next: (results: any) => {
+ if (s.lastQueryByIndex[index] !== term) return;
+ const list = results?.data?.sctMaster ?? [];
+
+ if (append) {
+ const existing = new Set(
+ (s.suggested[index] ?? []).map(
+ (d: any) => d.id ?? d.code ?? d.term,
+ ),
+ );
+ s.suggested[index] = [
+ ...(s.suggested[index] ?? []),
+ ...list.filter(
+ (d: any) => !existing.has(d.id ?? d.code ?? d.term),
+ ),
+ ];
+ } else {
+ s.suggested[index] = list;
+ }
+
+ s.pageByIndex[index] = nextLogical;
+ if (!list.length) s.noMore[index] = true;
+ },
+ complete: () => {
+ const wantChain = s.wantMore[index] && !s.noMore[index];
+ s.loadingMore[index] = false;
+ s.wantMore[index] = false;
+ if (wantChain) this.fetchPage(type, index, true);
+ },
+ });
+ }
}
diff --git a/src/app/app-modules/nurse-doctor/nurse-doctor.module.ts b/src/app/app-modules/nurse-doctor/nurse-doctor.module.ts
index 41c782c..b6f7aa6 100644
--- a/src/app/app-modules/nurse-doctor/nurse-doctor.module.ts
+++ b/src/app/app-modules/nurse-doctor/nurse-doctor.module.ts
@@ -160,6 +160,7 @@ import { GeneralCaseSheetComponent } from './case-sheet/general-case-sheet/gener
import { HistoryCaseSheetComponent } from './case-sheet/general-case-sheet/history-case-sheet/history-case-sheet.component';
import { PncCaseSheetComponent } from './case-sheet/general-case-sheet/pnc-case-sheet/pnc-case-sheet.component';
import { SharedModule } from '../core/components/shared/shared.module';
+import { AutocompleteScrollerDirective } from './shared/utility/autocomplete-scroller.directive';
@NgModule({
imports: [
@@ -291,6 +292,7 @@ import { SharedModule } from '../core/components/shared/shared.module';
NurseMmuTmReferredWorklistComponent,
DiseaseconfirmationComponent,
CovidVaccinationStatusComponent,
+ AutocompleteScrollerDirective,
],
providers: [
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..ab43773 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
@@ -622,88 +622,83 @@
-
+
-
diff --git a/src/app/app-modules/nurse-doctor/quick-consult/quick-consult.component.ts b/src/app/app-modules/nurse-doctor/quick-consult/quick-consult.component.ts
index d7954d5..5ea53cb 100644
--- a/src/app/app-modules/nurse-doctor/quick-consult/quick-consult.component.ts
+++ b/src/app/app-modules/nurse-doctor/quick-consult/quick-consult.component.ts
@@ -169,6 +169,17 @@ export class QuickConsultComponent
dataSource = new MatTableDataSource
();
+ suggestedDiagnosisList: any = [];
+ private readonly PAGE_BASE = 0;
+
+ private readonly BOOTSTRAP_MAX_PAGES = 3; // when first page can't scroll, prefill up to this many extra pages
+
+ loadingMore: boolean[] = [];
+ noMore: boolean[] = [];
+ wantMore: boolean[] = [];
+ pageByIndex: number[] = [];
+ lastQueryByIndex: string[] = [];
+
constructor(
private fb: FormBuilder,
private doctorService: DoctorService,
@@ -1294,35 +1305,38 @@ export class QuickConsultComponent
}
}
- removeDiagnosisFromList(
- index: any,
- diagnosisList: AbstractControl,
- ) {
- const diagnosisListForm = this.patientQuickConsultForm.controls[
+ deleteDiagnosis(index: any, diagnosisListForm: AbstractControl) {
+ const diagnosisListArray = this.patientQuickConsultForm.controls[
'provisionalDiagnosisList'
] as FormArray;
- if (!diagnosisListForm.at(index).invalid) {
+ if (diagnosisListArray.at(index).valid) {
this.confirmationService
.confirm(`warn`, this.currentLanguageSet.alerts.info.warn)
- .subscribe((result) => {
+ .subscribe(result => {
if (result) {
- if (diagnosisListForm.length > 1) {
- diagnosisListForm.removeAt(index);
+ if (diagnosisListArray.length > 1) {
+ diagnosisListArray.removeAt(index);
} else {
- diagnosisListForm.removeAt(index);
- diagnosisListForm.push(this.utils.initProvisionalDiagnosisList());
+ diagnosisListForm.reset();
+ (diagnosisListForm as FormGroup).controls[
+ 'viewProvisionalDiagnosisProvided'
+ ].enable();
}
+ this.patientQuickConsultForm.markAsDirty();
}
});
} else {
- if (diagnosisListForm.length > 1) {
- diagnosisListForm.removeAt(index);
+ if (diagnosisListArray.length > 1) {
+ diagnosisListArray.removeAt(index);
} else {
- diagnosisListForm.removeAt(index);
- diagnosisListForm.push(this.utils.initProvisionalDiagnosisList());
+ diagnosisListForm.reset();
+ (diagnosisListForm as FormGroup).controls[
+ 'viewProvisionalDiagnosisProvided'
+ ].enable();
}
}
}
+
checkProvisionalDiagnosisValidity(provisionalDiagnosis: any) {
const temp = provisionalDiagnosis.value;
if (temp.term && temp.conceptID) {
@@ -1351,4 +1365,139 @@ export class QuickConsultComponent
}
return false;
}
+
+ onDiagnosisInputKeyup(value: string, index: number) {
+ const term = (value || '').trim();
+
+ if (term.length >= 3) {
+ if (this.lastQueryByIndex[index] !== term) {
+ this.lastQueryByIndex[index] = term;
+ this.pageByIndex[index] = 0; // logical 0th page
+ this.noMore[index] = false;
+ this.wantMore[index] = false;
+ this.suggestedDiagnosisList[index] = [];
+ }
+ this.fetchPage(index, false);
+ } else {
+ this.lastQueryByIndex[index] = '';
+ this.pageByIndex[index] = 0;
+ this.noMore[index] = false;
+ this.wantMore[index] = false;
+ this.suggestedDiagnosisList[index] = [];
+ }
+ }
+
+ displayDiagnosis(diagnosis: any): string {
+ return typeof diagnosis === 'string' ? diagnosis : diagnosis?.term || '';
+ }
+
+ onDiagnosisSelected(selected: any, index: number) {
+ const diagnosisFormArray = this.patientQuickConsultForm.get(
+ 'provisionalDiagnosisList',
+ ) as FormArray;
+ const diagnosisFormGroup = diagnosisFormArray.at(index) as FormGroup;
+
+ // Set the nested and top-level fields
+ diagnosisFormGroup.patchValue({
+ provisionalDiagnosis: selected,
+ conceptID: selected?.conceptID || null,
+ term: selected?.term || null,
+ });
+ }
+
+ onPanelReady(index: number, panelEl: HTMLElement) {
+ if (panelEl.scrollHeight <= panelEl.clientHeight && !this.noMore[index]) {
+ this.bootstrapUntilScrollable(index, panelEl);
+ }
+ }
+
+ onAutoNearEnd(index: number) {
+ if (!this.loadingMore[index] && !this.noMore[index]) {
+ this.fetchPage(index, true);
+ } else if (this.loadingMore[index]) {
+ this.wantMore[index] = true;
+ }
+ }
+
+ private bootstrapUntilScrollable(rowIndex: number, panelEl: HTMLElement) {
+ let fetched = 0;
+
+ const tryFill = () => {
+ const scrollable = panelEl.scrollHeight > panelEl.clientHeight;
+ if (
+ scrollable ||
+ this.noMore[rowIndex] ||
+ fetched >= this.BOOTSTRAP_MAX_PAGES
+ )
+ return;
+
+ if (this.loadingMore[rowIndex]) {
+ requestAnimationFrame(tryFill);
+ return;
+ }
+
+ fetched++;
+ this.fetchPage(rowIndex, true);
+
+ requestAnimationFrame(tryFill);
+ };
+
+ if (this.lastQueryByIndex[rowIndex]?.length >= 3) {
+ tryFill();
+ }
+ }
+
+ private fetchPage(index: number, append = false) {
+ const term = this.lastQueryByIndex[index];
+ if (!term) return;
+
+ const nextLogical = (this.pageByIndex[index] ?? 0) + (append ? 1 : 0);
+ const pageAtReq = nextLogical + this.PAGE_BASE;
+
+ if (this.loadingMore[index]) return;
+ this.loadingMore[index] = true;
+
+ const termAtReq = term;
+
+ this.masterdataService
+ .searchDiagnosisBasedOnPageNo(termAtReq, pageAtReq)
+ .subscribe({
+ next: (results: any) => {
+ if (this.lastQueryByIndex[index] !== termAtReq) return;
+
+ const list = results?.data?.sctMaster ?? [];
+
+ if (append) {
+ const existing = new Set(
+ (this.suggestedDiagnosisList[index] ?? []).map(
+ (d: any) => d.id ?? d.code ?? d.term
+ )
+ );
+ this.suggestedDiagnosisList[index] = [
+ ...(this.suggestedDiagnosisList[index] ?? []),
+ ...list.filter(
+ (d: any) => !existing.has(d.id ?? d.code ?? d.term)
+ ),
+ ];
+ } else {
+ this.suggestedDiagnosisList[index] = list;
+ }
+
+ this.pageByIndex[index] = nextLogical;
+ if (!list.length) {
+ this.noMore[index] = true;
+ }
+ },
+ error: () => {
+ console.error('Error fetching diagnosis data');
+ },
+ complete: () => {
+ const wantChain = this.wantMore[index] && !this.noMore[index];
+ this.loadingMore[index] = false;
+ this.wantMore[index] = false;
+
+ if (wantChain) this.fetchPage(index, true);
+ },
+ });
+ }
}
diff --git a/src/app/app-modules/nurse-doctor/shared/utility/autocomplete-scroller.directive.ts b/src/app/app-modules/nurse-doctor/shared/utility/autocomplete-scroller.directive.ts
new file mode 100644
index 0000000..629a996
--- /dev/null
+++ b/src/app/app-modules/nurse-doctor/shared/utility/autocomplete-scroller.directive.ts
@@ -0,0 +1,119 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+import {
+ Directive,
+ EventEmitter,
+ NgZone,
+ OnDestroy,
+ AfterViewInit,
+ Output,
+ Input,
+} from '@angular/core';
+import { MatAutocomplete } from '@angular/material/autocomplete';
+import { Subscription } from 'rxjs';
+
+@Directive({
+ selector: '[appAutocompleteScroller]',
+})
+export class AutocompleteScrollerDirective implements AfterViewInit, OnDestroy {
+ @Input() threshold = 0.6; // 60% down
+ @Output() nearEnd = new EventEmitter();
+ @Output() panelReady = new EventEmitter(); // lets component bootstrap if no overflow
+
+ private subs: Subscription[] = [];
+ private scrollListener?: (e: Event) => void;
+
+ constructor(
+ private ac: MatAutocomplete,
+ private ngZone: NgZone,
+ ) {}
+
+ ngAfterViewInit(): void {
+ // Fired when the overlay panel opens
+ const openedSub = this.ac.opened.subscribe(() => {
+ this.waitForPanel((panelEl) => {
+ this.panelReady.emit(panelEl); // tell component the panel is live (for bootstrap)
+
+ // attach scroll listener
+ this.ngZone.runOutsideAngular(() => {
+ this.scrollListener = () => {
+ if (panelEl.scrollHeight <= panelEl.clientHeight) return; // no overflow yet
+ const ratio =
+ (panelEl.scrollTop + panelEl.clientHeight) / panelEl.scrollHeight;
+ if (ratio >= this.threshold) {
+ // hop back into Angular so change detection works
+ this.ngZone.run(() => this.nearEnd.emit());
+ }
+ };
+ panelEl.addEventListener('scroll', this.scrollListener!, {
+ passive: true,
+ });
+ });
+ });
+ });
+
+ // Fired when the panel closes
+ const closedSub = this.ac.closed.subscribe(() => {
+ const panelEl = this.getPanelEl();
+ if (panelEl && this.scrollListener) {
+ panelEl.removeEventListener('scroll', this.scrollListener);
+ this.scrollListener = undefined;
+ }
+ });
+
+ this.subs.push(openedSub, closedSub);
+ }
+
+ ngOnDestroy(): void {
+ this.subs.forEach((s) => s.unsubscribe());
+ const panelEl = this.getPanelEl();
+ if (panelEl && this.scrollListener) {
+ panelEl.removeEventListener('scroll', this.scrollListener);
+ this.scrollListener = undefined;
+ }
+ }
+
+ private getPanelEl(): HTMLElement | null {
+ // Newer Material exposes panel element
+ const anyAc = this.ac as any;
+ const el = anyAc?.panel?.nativeElement as HTMLElement | null | undefined;
+ if (el) return el;
+
+ // Fallback by id
+ const id = anyAc?.id as string | undefined;
+ return id ? (document.getElementById(id) as HTMLElement | null) : null;
+ }
+
+ private waitForPanel(cb: (panelEl: HTMLElement) => void) {
+ let tries = 0;
+ const maxTries = 10;
+ const tick = () => {
+ const el = this.getPanelEl();
+ if (el) {
+ cb(el);
+ return;
+ }
+ if (tries++ < maxTries) requestAnimationFrame(tick);
+ };
+ requestAnimationFrame(tick);
+ }
+}