From 417f638a9af8b96d35cfc7c1ad41d03349423020 Mon Sep 17 00:00:00 2001 From: Alfred-Mutai Date: Mon, 1 Dec 2025 12:05:09 +0300 Subject: [PATCH 1/2] POC-1232: Updated case surveillance linelist issues --- .../hiv/data-analytics-hiv.routes.ts | 15 + .../etl-api/case-surveillance.service.spec.ts | 18 + src/app/etl-api/case-surveillance.service.ts | 58 +++ .../case-surveillance-base.component.css | 0 .../case-surveillance-base.component.html | 31 ++ .../case-surveillance-base.component.spec.ts | 24 + .../case-surveillance-base.component.ts | 223 +++++++++ ...se-surveillance-patient-list.component.css | 0 ...e-surveillance-patient-list.component.html | 34 ++ ...urveillance-patient-list.component.spec.ts | 24 + ...ase-surveillance-patient-list.component.ts | 112 +++++ .../facility-dashboard.component.css | 143 ++++++ .../facility-dashboard.component.html | 86 ++++ .../facility-dashboard.component.spec.ts | 24 + .../facility-dashboard.component.ts | 431 ++++++++++++++++++ .../dqa-reports/dqa-reports.component.html | 2 +- .../dqa-reports/dqa-reports.component.ts | 19 +- .../dqa-reports/dqa-reports/dqa-reports.json | 5 + src/app/hiv-care-lib/hiv-care-lib.module.ts | 8 +- 19 files changed, 1250 insertions(+), 7 deletions(-) create mode 100644 src/app/etl-api/case-surveillance.service.spec.ts create mode 100644 src/app/etl-api/case-surveillance.service.ts create mode 100644 src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-base/case-surveillance-base.component.css create mode 100644 src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-base/case-surveillance-base.component.html create mode 100644 src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-base/case-surveillance-base.component.spec.ts create mode 100644 src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-base/case-surveillance-base.component.ts create mode 100644 src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-patient-list/case-surveillance-patient-list.component.css create mode 100644 src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-patient-list/case-surveillance-patient-list.component.html create mode 100644 src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-patient-list/case-surveillance-patient-list.component.spec.ts create mode 100644 src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-patient-list/case-surveillance-patient-list.component.ts create mode 100644 src/app/hiv-care-lib/dqa-reports/case-surveillance/facility-dashboard/facility-dashboard.component.css create mode 100644 src/app/hiv-care-lib/dqa-reports/case-surveillance/facility-dashboard/facility-dashboard.component.html create mode 100644 src/app/hiv-care-lib/dqa-reports/case-surveillance/facility-dashboard/facility-dashboard.component.spec.ts create mode 100644 src/app/hiv-care-lib/dqa-reports/case-surveillance/facility-dashboard/facility-dashboard.component.ts diff --git a/src/app/data-analytics-dashboard/hiv/data-analytics-hiv.routes.ts b/src/app/data-analytics-dashboard/hiv/data-analytics-hiv.routes.ts index e9e7a5919..2a04aba4e 100644 --- a/src/app/data-analytics-dashboard/hiv/data-analytics-hiv.routes.ts +++ b/src/app/data-analytics-dashboard/hiv/data-analytics-hiv.routes.ts @@ -57,6 +57,8 @@ import { AhdReportComponent } from './ahd-report/ahd-report.component'; import { AhdMonthlyReportPatientlistComponent } from 'src/app/hiv-care-lib/ahd-monthly-report/ahd-monthly-report-patientlist/ahd-monthly-report-patientlist.component'; import { PlhivNcdV2ReportPatientListComponent } from 'src/app/hiv-care-lib/plhiv-ncd-v2-report/plhiv-ncd-v2-report-patient-list/plhiv-ncd-v2-report-patient-list.component'; import { PlhivNcdV2ReportComponent } from './plhiv-ncd-v2-report/plhiv-ncd-v2-report.component'; +import { CaseSurveillanceBaseComponent } from 'src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-base/case-surveillance-base.component'; +import { CaseSurveillancePatientListComponent } from 'src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-patient-list/case-surveillance-patient-list.component'; const routes: Routes = [ { @@ -375,6 +377,19 @@ const routes: Routes = [ } ] }, + { + path: 'case-surveillance', + children: [ + { + path: '', + component: CaseSurveillanceBaseComponent + }, + { + path: 'cs-report-patientlist', + component: CaseSurveillancePatientListComponent + } + ] + }, { path: '', component: DqaReportsComponent, diff --git a/src/app/etl-api/case-surveillance.service.spec.ts b/src/app/etl-api/case-surveillance.service.spec.ts new file mode 100644 index 000000000..47264649d --- /dev/null +++ b/src/app/etl-api/case-surveillance.service.spec.ts @@ -0,0 +1,18 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { CaseSurveillanceService } from './case-surveillance.service'; + +describe('CaseSurveillanceService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [CaseSurveillanceService] + }); + }); + + it('should be created', inject( + [CaseSurveillanceService], + (service: CaseSurveillanceService) => { + expect(service).toBeTruthy(); + } + )); +}); diff --git a/src/app/etl-api/case-surveillance.service.ts b/src/app/etl-api/case-surveillance.service.ts new file mode 100644 index 000000000..99fd0e5ba --- /dev/null +++ b/src/app/etl-api/case-surveillance.service.ts @@ -0,0 +1,58 @@ +import { Injectable } from '@angular/core'; +import { AppSettingsService } from '../app-settings/app-settings.service'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import * as Moment from 'moment'; +import { catchError, map } from 'rxjs/operators'; +@Injectable({ + providedIn: 'root' +}) +export class CaseSurveillanceService { + public get url(): string { + return this.appSettingsService.getEtlRestbaseurl().trim(); + } + constructor( + public http: HttpClient, + public appSettingsService: AppSettingsService + ) {} + public getCaseSurveillanceReport(params: any): Observable { + // tslint:disable-next-line: max-line-length + return this.http + .get( + `${this.url}cs-case-surveillance?startDate=${params.startDate}&endDate=${params.endDate}&locationUuids=${params.locationUuids}` + ) + .pipe( + catchError((err: any) => { + const error: any = err; + const errorObj = { + error: error.status, + message: error.statusText + }; + return Observable.of(errorObj); + }), + map((response: Response) => { + return response; + }) + ); + } + public getCaseSurveillancePatientList(params: any): Observable { + // tslint:disable-next-line: max-line-length + return this.http + .get( + `${this.url}cs-case-surveillance-patient-list?startDate=${params.startDate}&endDate=${params.endDate}&locationUuids=${params.locationUuids}&indicators=${params.indicators}` + ) + .pipe( + catchError((err: any) => { + const error: any = err; + const errorObj = { + error: error.status, + message: error.statusText + }; + return Observable.of(errorObj); + }), + map((response: Response) => { + return response; + }) + ); + } +} diff --git a/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-base/case-surveillance-base.component.css b/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-base/case-surveillance-base.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-base/case-surveillance-base.component.html b/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-base/case-surveillance-base.component.html new file mode 100644 index 000000000..58ddaeef6 --- /dev/null +++ b/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-base/case-surveillance-base.component.html @@ -0,0 +1,31 @@ +

+ Case Surveillance +

+ + + + +
+ × +

+ An + error occurred while trying to load the report. Please try again. +

+

+ {{ errorMessage }} +

+
+
+ +
diff --git a/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-base/case-surveillance-base.component.spec.ts b/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-base/case-surveillance-base.component.spec.ts new file mode 100644 index 000000000..2224e2ba6 --- /dev/null +++ b/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-base/case-surveillance-base.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CaseSurveillanceBaseComponent } from './case-surveillance-base.component'; + +describe('CaseSurveillanceBaseComponent', () => { + let component: CaseSurveillanceBaseComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [CaseSurveillanceBaseComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CaseSurveillanceBaseComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-base/case-surveillance-base.component.ts b/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-base/case-surveillance-base.component.ts new file mode 100644 index 000000000..a952bbfb8 --- /dev/null +++ b/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-base/case-surveillance-base.component.ts @@ -0,0 +1,223 @@ +import { Component, OnInit } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; + +import * as Moment from 'moment'; +import * as _ from 'lodash'; +import { take } from 'rxjs/operators'; +import { DataAnalyticsDashboardService } from 'src/app/data-analytics-dashboard/services/data-analytics-dashboard.services'; +import { CaseSurveillanceService } from 'src/app/etl-api/case-surveillance.service'; + +@Component({ + selector: 'app-case-surveillance-base', + templateUrl: './case-surveillance-base.component.html', + styleUrls: ['./case-surveillance-base.component.css'] +}) +export class CaseSurveillanceBaseComponent implements OnInit { + public enabledControls = 'locationControl,datesControl,'; + public _locationUuids: any = []; + public params: any = []; + public showPatientList = false; + public showInfoMessage = false; + public facilityName = ''; + public csSummaryData: any = []; + private _startDate: Date = Moment().subtract(1, 'month').toDate(); + isLoading: boolean; + columnDefs: any; + errorMessage: string; + pinnedBottomRowData: any[]; + private _sDate: any; + private _eDate: any; + public get startDate(): Date { + return this._startDate; + } + + public set startDate(v: Date) { + this._startDate = v; + } + + private _endDate: Date = new Date(); + public get endDate(): Date { + return this._endDate; + } + + public set endDate(v: Date) { + this._endDate = v; + } + public get locationUuids(): Array { + return this._locationUuids; + } + + public set locationUuids(v: Array) { + const locationUuids = []; + _.each(v, (location: any) => { + if (location.value) { + locationUuids.push(location); + } + }); + this._locationUuids = locationUuids; + } + constructor( + private router: Router, + private dataAnalyticsDashboardService: DataAnalyticsDashboardService, + private route: ActivatedRoute, + public caseSurveillanceService: CaseSurveillanceService + ) {} + + ngOnInit() {} + + // ✅ Proper reusable function to build query params + private buildQueryParams(selectedLocations: any) { + return (this.params = { + locationUuids: this.getSelectedLocations(selectedLocations), + startDate: Moment(this.startDate).format('YYYY-MM-DD'), + endDate: Moment(this.endDate).format('YYYY-MM-DD') + }); + } + + public getCaseSurveillanceReport(params: any) { + this.isLoading = true; + this.caseSurveillanceService + .getCaseSurveillanceReport(params) + .subscribe((data) => { + try { + if (data.error) { + this.showInfoMessage = true; + this.errorMessage = `There has been an error while loading the report, please retry again`; + this.isLoading = false; + return; + } + + this.showInfoMessage = false; + + // Extract all results from queriesAndSchemas + let allResults: any[] = []; + if (Array.isArray(data.queriesAndSchemas)) { + data.queriesAndSchemas.forEach((reportItem) => { + if (reportItem.results && reportItem.results.results) { + allResults = allResults.concat(reportItem.results.results); + } + }); + } + + this.csSummaryData = allResults; + this.columnDefs = data.sectionDefinitions || []; + + this.calculateTotalSummary(); + } catch (err) { + console.error('Error processing report', err); + this.showInfoMessage = true; + this.errorMessage = 'There was an error processing the report.'; + } finally { + this.isLoading = false; + } + }); + } + + public calculateTotalSummary() { + const totalsRow = []; + const totalObj: any = { location: 'Totals' }; + + if (this.csSummaryData && this.csSummaryData.length > 0) { + this.csSummaryData.forEach((item) => { + if (item.results && Array.isArray(item.results)) { + item.results.forEach((row) => { + Object.keys(row).forEach((key) => { + if (typeof row[key] === 'number') { + // Add numeric values + if (totalObj[key]) { + totalObj[key] += row[key]; + } else { + totalObj[key] = row[key]; + } + } else { + // Ensure non-numeric fields exist + if (!totalObj[key]) { + totalObj[key] = null; + } + } + }); + }); + } + }); + totalsRow.push(totalObj); + this.pinnedBottomRowData = totalsRow; + } + } + + public calculateTotalSummary1() { + const totalsRow = []; + if (this.csSummaryData.length > 0) { + const totalObj = { + location: 'Totals' + }; + _.each(this.csSummaryData, (row) => { + Object.keys(row).map((key) => { + if (Number.isInteger(row[key]) === true) { + if (totalObj[key]) { + totalObj[key] = row[key] + totalObj[key]; + } else { + totalObj[key] = row[key]; + } + } else { + if (Number.isNaN(totalObj[key])) { + totalObj[key] = 0; + } + if (totalObj[key] === null) { + totalObj[key] = 0; + } + totalObj[key] = 0 + totalObj[key]; + } + }); + }); + totalObj.location = 'Totals'; + totalsRow.push(totalObj); + this.pinnedBottomRowData = totalsRow; + } + } + + public onIndicatorSelected(value) { + this.router.navigate(['patient-list'], { + relativeTo: this.route, + queryParams: { + indicators: value.field, + indicatorHeader: value.headerName, + indicatorGender: value.gender, + sDate: this._sDate, + eDate: this._eDate, + locationUuids: value.location + } + }); + } + + // ✅ Generate report and navigate + public generateReport() { + this.dataAnalyticsDashboardService + .getSelectedLocations() + .pipe(take(1)) + .subscribe((data) => { + if (data) { + const queryParams = this.buildQueryParams(data.locations); + + this.router.navigate([], { + relativeTo: this.route, + queryParams + }); + + this.facilityName = data.facility + ? data.facility + : data.locations.length > 0 + ? data.locations[0].label + : ''; + + this.getCaseSurveillanceReport(this.params); + + this.showPatientList = true; + } + }); + } + + // Convert location objects → CSV string + private getSelectedLocations(locationUuids: Array): string { + return locationUuids.map((location) => location.value).join(','); + } +} diff --git a/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-patient-list/case-surveillance-patient-list.component.css b/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-patient-list/case-surveillance-patient-list.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-patient-list/case-surveillance-patient-list.component.html b/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-patient-list/case-surveillance-patient-list.component.html new file mode 100644 index 000000000..d73f09de5 --- /dev/null +++ b/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-patient-list/case-surveillance-patient-list.component.html @@ -0,0 +1,34 @@ +
+ +
+
+ Loading... +
+

+ {{ selectedIndicator }} patient list +

+
+ + +
+

+ + All records loaded {{ '[ ' + patientData.length + ' ]' }} +

+ +

+
+ +
+

Error loading patient list.

+
diff --git a/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-patient-list/case-surveillance-patient-list.component.spec.ts b/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-patient-list/case-surveillance-patient-list.component.spec.ts new file mode 100644 index 000000000..3993898a6 --- /dev/null +++ b/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-patient-list/case-surveillance-patient-list.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CaseSurveillancePatientListComponent } from './case-surveillance-patient-list.component'; + +describe('CaseSurveillancePatientListComponent', () => { + let component: CaseSurveillancePatientListComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [CaseSurveillancePatientListComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CaseSurveillancePatientListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-patient-list/case-surveillance-patient-list.component.ts b/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-patient-list/case-surveillance-patient-list.component.ts new file mode 100644 index 000000000..117fbe23d --- /dev/null +++ b/src/app/hiv-care-lib/dqa-reports/case-surveillance/case-surveillance-patient-list/case-surveillance-patient-list.component.ts @@ -0,0 +1,112 @@ +import { Component, OnInit } from '@angular/core'; +import { CaseSurveillanceService } from 'src/app/etl-api/case-surveillance.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Location } from '@angular/common'; +import * as Moment from 'moment'; +@Component({ + selector: 'app-case-surveillance-patient-list', + templateUrl: './case-surveillance-patient-list.component.html', + styleUrls: ['./case-surveillance-patient-list.component.css'] +}) +export class CaseSurveillancePatientListComponent implements OnInit { + public params: any; + public patientData: any; + public extraColumns: Array = []; + public isLoading = true; + public overrideColumns: Array = []; + public selectedIndicator: string; + public hasLoadedAll = false; + public hasError = false; + public selectedMonth: String; + + constructor( + private router: Router, + private route: ActivatedRoute, + private _location: Location, + public caseSurveillanceService: CaseSurveillanceService + ) {} + + ngOnInit() { + this.route.queryParams.subscribe( + (params) => { + if (params && params.startDate && params.endDate) { + this.params = params; + this.selectedIndicator = params.indicatorHeader; + this.getPatientList(params); + } + }, + (error) => { + console.error('Error', error); + } + ); + this.addExtraColumns(); + } + + private getPatientList(params: any) { + this.caseSurveillanceService + .getCaseSurveillancePatientList(params) + .subscribe((data) => { + this.isLoading = false; + this.patientData = data.results.results; + this.hasLoadedAll = true; + }); + } + + public addExtraColumns() { + const extraColumns = { + weight: 'Weight', + phone_number: 'Phone', + enrollment_date: 'Enrolment Date', + last_appointment: 'Last Appointment', + latest_rtc_date: 'Latest RTC Date', + days_since_rtc_date: 'Days since RTC', + arv_first_regimen: 'ARV first regimen', + cd4_1: 'CD4', + cd4_1_date: 'CD4 Date', + arv_first_regimen_start_date: 'First ARV start date', + cur_meds: 'Current Regimen', + cur_arv_line: 'Current ARV Line', + arv_start_date: 'ARV Start Date', + latest_vl: 'Latest VL', + vl_category: 'VL Category', + latest_vl_date: 'Latest VL Date', + previous_vl: 'Previous VL', + previous_vl_date: 'Previous VL Date', + ovcid_id: 'OVCID' + }; + + for (const indicator in extraColumns) { + if (indicator) { + this.extraColumns.push({ + headerName: extraColumns[indicator], + field: indicator + }); + } + } + + this.overrideColumns.push( + { + field: 'identifiers', + cellRenderer: (column) => { + return ( + '' + + column.value + + '' + ); + } + }, + { + field: 'last_appointment', + width: 200 + }, + { + field: 'cur_prep_meds_names', + width: 160 + } + ); + } + + public goBack() { + this._location.back(); + } +} diff --git a/src/app/hiv-care-lib/dqa-reports/case-surveillance/facility-dashboard/facility-dashboard.component.css b/src/app/hiv-care-lib/dqa-reports/case-surveillance/facility-dashboard/facility-dashboard.component.css new file mode 100644 index 000000000..77a130c0b --- /dev/null +++ b/src/app/hiv-care-lib/dqa-reports/case-surveillance/facility-dashboard/facility-dashboard.component.css @@ -0,0 +1,143 @@ +/* Main container */ +.dashboard-container { + padding: 20px; + font-family: Arial, sans-serif; + color: #333; +} + +/* Header */ +.header { + display: flex; + justify-content: space-between; + margin-bottom: 25px; +} + +.header h4 { + margin: 0; + font-size: 18px; +} + +.timestamp { + font-size: 14px; + color: #555; +} + +/* Tabs */ +.tabs { + display: flex; + margin-bottom: 20px; + border-bottom: 2px solid #ddd; +} + +.tab { + padding: 10px 20px; + border: none; + background: none; + cursor: pointer; + font-size: 15px; + margin-right: 10px; + color: #666; + border-bottom: 3px solid transparent; +} + +.tab.active { + color: #000; + font-weight: bold; + border-bottom: 3px solid #4caf50; +} + +/* Filters */ +.filters { + display: flex; + gap: 20px; + margin-bottom: 25px; +} + +.filter-item { + display: flex; + flex-direction: column; +} + +.filter-item label { + font-size: 12px; + margin-bottom: 4px; +} + +/* Cards grid */ +.card-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 15px; +} + +.card { + background: #fafafa; + border-radius: 8px; + padding: 15px; + border: 1px solid #e0e0e0; +} + +.card-header { + display: flex; + justify-content: space-between; + margin-bottom: 10px; +} + +.card-header h4 { + font-size: 15px; + font-weight: bold; + margin: 0; +} + +.dot { + width: 14px; + height: 14px; + border-radius: 50%; +} + +.red { + background-color: #ff4d4d; +} + +.green { + background-color: #4caf50; +} + +.card-value { + font-size: 18px; + margin-bottom: 8px; + font-weight: bold; +} + +.percent { + font-size: 12px; + color: #777; +} + +.card-desc { + font-size: 12px !important; + color: #666; +} + +/* Progress Tracker Section */ +.progress-container { + margin-top: 20px; +} + +.progress-box { + background: #f5f5f5; + padding: 15px; + margin-bottom: 15px; + border-radius: 8px; + border: 1px solid #ddd; +} + +.clickable { + cursor: pointer; + color: #007bff; + font-weight: bold; +} + +.clickable:hover { + text-decoration: underline; +} diff --git a/src/app/hiv-care-lib/dqa-reports/case-surveillance/facility-dashboard/facility-dashboard.component.html b/src/app/hiv-care-lib/dqa-reports/case-surveillance/facility-dashboard/facility-dashboard.component.html new file mode 100644 index 000000000..445bdfcea --- /dev/null +++ b/src/app/hiv-care-lib/dqa-reports/case-surveillance/facility-dashboard/facility-dashboard.component.html @@ -0,0 +1,86 @@ +
+ +
+
+

{{ title }}

+
+
+ {{ facilityName }} +
+
+ + +
+ + + +
+ + +
+
+
+

{{ item.title }}

+ + +
+ +
Loading...
+
+ + {{ item.numerator[0] }} + + + / + + + {{ item.denominator[0] }} + + + ({{ item.percent }}%) +
+ +

{{ item.description }}

+
+
+ + +
+

This section will display progress indicators, graphs, or KPIs.

+ + +
+

Linkage Progress

+

achieved

+
+ +
+

VL Suppression Progress

+

achieved

+
+
+
diff --git a/src/app/hiv-care-lib/dqa-reports/case-surveillance/facility-dashboard/facility-dashboard.component.spec.ts b/src/app/hiv-care-lib/dqa-reports/case-surveillance/facility-dashboard/facility-dashboard.component.spec.ts new file mode 100644 index 000000000..8a2f89814 --- /dev/null +++ b/src/app/hiv-care-lib/dqa-reports/case-surveillance/facility-dashboard/facility-dashboard.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FacilityDashboardComponent } from './facility-dashboard.component'; + +describe('FacilityDashboardComponent', () => { + let component: FacilityDashboardComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [FacilityDashboardComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FacilityDashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/hiv-care-lib/dqa-reports/case-surveillance/facility-dashboard/facility-dashboard.component.ts b/src/app/hiv-care-lib/dqa-reports/case-surveillance/facility-dashboard/facility-dashboard.component.ts new file mode 100644 index 000000000..22c1be2cc --- /dev/null +++ b/src/app/hiv-care-lib/dqa-reports/case-surveillance/facility-dashboard/facility-dashboard.component.ts @@ -0,0 +1,431 @@ +import { + Component, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges +} from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { DomSanitizer } from '@angular/platform-browser'; + +@Component({ + selector: 'app-facility-dashboard', + templateUrl: './facility-dashboard.component.html', + styleUrls: ['./facility-dashboard.component.css'] +}) +export class FacilityDashboardComponent implements OnInit, OnChanges { + @Input() facilityName!: string; + @Input() SummaryData = []; + @Input() sectionDefs: any; + @Input() reportDetails: any = []; + + @Output() + public indicatorSelected = new EventEmitter(); + private _rowDefs: Array; + public test = []; + public gridOptions: any = { + columnDefs: [] + }; + public pdfvalue: any; + public pdfSrc: string = null; + public isBusy = false; + public multipleLocations = false; + public headers = []; + public sectionIndicatorsValues: Array; + public pdfWidth = 1; + public page = 1; + public selectedResult: string; + public selectedIndicatorsList = []; + public errorFlag = false; + public pdfMakeProxy: any = null; + newCase = 0; + linkedCase = 0; + atRiskPbfw = 0; + eligibleforVl = 0; + hei6to8Weeks = 0; + hei24Months = 0; + heiWithoutFinalOutcome = 0; + heiWithoutPcr = 0; + prepLinked = 0; + eligibleForVlNoOrder = 0; + isLoadingCards = false; + + public cards: any[] = []; + activeTab = 'realtime'; + title = 'Facility Dashboard'; + + public startDate: string; + public endDate: string; + public locationUuids: string; + + @Output() + public CellSelection = new EventEmitter(); + + constructor(private router: Router, private route: ActivatedRoute) {} + + ngOnInit() {} + + public ngOnChanges(changes: SimpleChanges) { + if (changes.SummaryData && this.SummaryData) { + this.isLoadingCards = true; + + // Wait for Angular to finish current change detection + Promise.resolve().then(() => { + this.updateCases(); + this.isLoadingCards = false; + }); + } + } + + private updateCases() { + this.sectionIndicatorsValues = this.SummaryData; + + // 🔹 Extract sum of results instead of first element + const extractValue = function ( + col: any, + customKey?: string, + fallback?: number + ): number { + fallback = fallback || 0; + + if (col && col.results && col.results.length > 0) { + return col.results.reduce(function (sum, row) { + const value = customKey ? row[customKey] : row.total; + return sum + (value ? value : 0); + }, 0); + } + + return fallback; + }; + + // 🔹 Map indicators + const indicatorConfig = [ + { key: 'atRiskPbfw', index: 0, custom: 'atRiskPbfwTotal' }, + { key: 'eligibleforVl', index: 1, custom: 'eligibleForVlTotal' }, + { key: 'hei6to8Weeks', index: 2, custom: 'hei6to8WeeksTotal' }, + { key: 'hei24Months', index: 3, custom: 'hei24MonthsTotal' }, + { + key: 'heiWithoutFinalOutcome', + index: 4, + custom: 'heiWithoutFinalOutcomeTotal' + }, + { key: 'heiWithoutPcr', index: 5, custom: 'heiWithoutPcrTotal' }, + { key: 'linkedCase', index: 6, custom: 'linkedCaseTotal' }, + { key: 'newCase', index: 7, custom: 'newCaseTotal' }, + { key: 'prepLinked', index: 8, custom: 'prepLinkedTotal' }, + { + key: 'eligibleForVlNoOrder', + index: 9, + custom: 'eligibleForVlNoOrderAggregate' + } + ]; + + // 🔹 Auto-assign values + indicatorConfig.forEach((item) => { + const column = this.sectionIndicatorsValues[item.index]; + this[item.key] = extractValue(column, item.custom); + }); + + // 🔹 Continue rendering + this.updateCards(); + if (this.sectionDefs) { + this.setColumns(this.sectionDefs); + } + } + + switchTab(tab: string) { + this.activeTab = tab; + } + + private updateCards() { + this.cards = [ + { + title: 'HIV +ve not linked', + numerator: [this.linkedCase, 'linkedCaseTotal'], + denominator: [this.newCase, 'newCaseTotal'], + percent: + this.newCase !== 0 + ? ((this.linkedCase / this.newCase) * 100).toFixed(1) + : 0, + description: 'HIV Positive Clients not linked to ART' + }, + { + title: 'High risk -ve PBFW not on PrEP', + numerator: [this.prepLinked, 'prepLinkedTotal'], + denominator: [this.atRiskPbfw, 'atRiskPbfwTotal'], + percent: + this.atRiskPbfw !== 0 + ? ((this.prepLinked / this.atRiskPbfw) * 100).toFixed(1) + : 0, + description: 'High risk -ve PBFW Not linked to PrEP' + }, + { + title: 'Delayed enhanced adherence counselling', + numerator: [0, ''], + denominator: [0, ''], + percent: 0, + description: 'Virally unsuppressed clients without EAC within 2 weeks' + }, + { + title: 'Missed opportunity in viral load testing', + numerator: [this.eligibleForVlNoOrder, 'eligibleForVlNoOrderTotal'], + denominator: [this.eligibleforVl, 'eligibleForVlTotal'], + percent: + this.eligibleforVl !== 0 + ? ((this.eligibleForVlNoOrder / this.eligibleforVl) * 100).toFixed( + 1 + ) + : 0, + description: + 'Number of clients on ART that visited the facility and were eligible for VL but no VL was done' + }, + { + title: 'HEI (6–8 weeks) without DNA-PCR Results', + numerator: [this.heiWithoutPcr, 'heiWithoutPcrTotal'], + denominator: [this.hei6to8Weeks, 'hei6to8WeeksTotal'], + percent: + this.hei6to8Weeks !== 0 + ? ((this.heiWithoutPcr / this.hei6to8Weeks) * 100).toFixed(1) + : 0, + description: 'HEI (6–8 WEEKS) without DNA PCR Results' + }, + { + title: 'Undocumented final outcome', + numerator: [this.heiWithoutFinalOutcome, 'heiWithoutFinalOutcomeTotal'], + denominator: [this.hei24Months, 'hei24MonthsTotal'], + percent: + this.hei24Months !== 0 + ? ((this.heiWithoutFinalOutcome / this.hei24Months) * 100).toFixed( + 1 + ) + : 0, + description: '24 months old HEI without documented outcome' + } + ]; + } + + openPatientList(item: any, type: string) { + const params = this.route.snapshot.queryParams; + + const startDate = params['startDate']; + const endDate = params['endDate']; + const indicators = + type === 'numerator' ? item.numerator[1] : item.denominator[1]; + const locationUuids = params['locationUuids']; + const indicatorName = + item.title + ' - ' + (type === 'numerator' ? 'Numerator' : 'Denominator'); + + this.router.navigate(['cs-report-patientlist'], { + relativeTo: this.route, + queryParams: { + startDate, + endDate, + locationUuids, + indicators, + indicatorHeader: indicatorName + } + }); + } + + public setColumns(sectionsData: Array) { + this.headers = []; + const defs = []; + + for (let i = 0; i < sectionsData.length; i++) { + const section = sectionsData[i]; + + const created: any = { + headerName: section.sectionTitle, + children: [] + }; + + this.headers.push({ + label: section.sectionTitle, + value: i + }); + + // Loop indicators + for (let j = 0; j < section.indicators.length; j++) { + const indicator = section.indicators[j]; + + // Normalize indicator into array of fields + let fields: any[] = []; + if (Array.isArray(indicator.indicator)) { + fields = indicator.indicator; + } else { + fields = [indicator.indicator]; + } + + const child: any = { + headerName: indicator.label, + field: indicator.indicator, + description: indicator.description, + value: [], + width: 360, + total: 0 + }; + + let sumOfValue = 0; + const locations = []; + + // Loop values returned from query + for (let k = 0; k < this.sectionIndicatorsValues.length; k++) { + const element = this.sectionIndicatorsValues[k]; + + const val: any = { + location: element['location_uuid'], + mfl_code: element['mfl_code'], + county: element['county'], + facility: element['facility'], + value: [] + }; + + // Multi-field indicator (ex: male/female OR ["total"]) + if (fields.length > 1) { + for (let f = 0; f < fields.length; f++) { + const fieldName = fields[f]; + const v = element[fieldName]; + + if (v !== undefined && v !== null) { + val.value.push(v); + sumOfValue += v; + } else { + val.value.push('-'); + } + } + } + + // Single field indicator (ex: "location" or "total") + if (fields.length === 1) { + const fieldName = fields[0]; + const v = element[fieldName]; + + if (v !== undefined && v !== null) { + val.value.push(v); + sumOfValue += v; + } else { + val.value.push('-'); + } + } + + locations.push(element['location_uuid']); + child.value.push(val); + } + + // Assign totals + child.total = { + location: locations, + value: sumOfValue + }; + + created.children.push(child); + } + + defs.push(created); + } + + this.gridOptions.columnDefs = defs; + } + + public setCellSelection(col, val, arrayPosition, grid) { + const arraypos = arrayPosition === 3 ? 0 : arrayPosition; + const selectedIndicator = { + headerName: col.headerName, + field: col.field[arraypos], + location: val.location + }; + this.CellSelection.emit(selectedIndicator); + } + public searchIndicator() { + this.setColumns(this.sectionDefs); + if (this.selectedResult.length > 0) { + this.gridOptions.columnDefs.forEach((object) => { + const make = { + headerName: '', + children: [] + }; + object.children.forEach((object2) => { + if ( + object2['headerName'].toLowerCase().match(this.selectedResult) !== + null + ) { + make.headerName = object['headerName']; + make.children.push(object2); + } + }); + if (make.headerName !== '') { + this.test.push(make); + } + }); + this.gridOptions.columnDefs = []; + this.gridOptions.columnDefs = this.test; + this.test = []; + } else { + this.setColumns(this.sectionDefs); + } + } + public selectedIndicators() { + this.setColumns(this.sectionDefs); + const value = []; + if (this.selectedIndicatorsList.length) { + this.selectedIndicatorsList.forEach((indicator) => { + value.push(this.gridOptions.columnDefs[indicator]); + }); + this.gridOptions.columnDefs = value; + } else { + this.setColumns(this.sectionDefs); + } + } + + public bodyValues() { + const body = []; + // let span = 0; + this.gridOptions.columnDefs.forEach((columnDefs) => { + const head = []; + const part = { + text: columnDefs.headerName, + style: 'tableHeader', + fillColor: '#337ab7', + colSpan: this.sectionIndicatorsValues.length + this.pdfWidth, + alignment: 'left' + }; + head.push(part); + body.push(head); + columnDefs.children.forEach((col) => { + const sec = []; + const test = { + text: col.headerName, + style: 'subheader', + alignment: 'left' + }; + sec.push(test); + col.value.forEach((element) => { + const value = { + text: element.value, + style: 'subheader', + alignment: 'center' + }; + sec.push(value); + }); + if (this.multipleLocations) { + sec.push({ + text: col.total.value, + style: 'title', + alignment: 'centre' + }); + } + body.push(sec); + }); + }); + return body; + } + + public nextPage(): void { + this.page++; + } + public prevPage(): void { + this.page--; + } +} diff --git a/src/app/hiv-care-lib/dqa-reports/dqa-reports/dqa-reports.component.html b/src/app/hiv-care-lib/dqa-reports/dqa-reports/dqa-reports.component.html index e4f1d2785..8cc596fcd 100644 --- a/src/app/hiv-care-lib/dqa-reports/dqa-reports/dqa-reports.component.html +++ b/src/app/hiv-care-lib/dqa-reports/dqa-reports/dqa-reports.component.html @@ -3,7 +3,7 @@

{{ title }}

diff --git a/src/app/hiv-care-lib/dqa-reports/dqa-reports/dqa-reports.component.ts b/src/app/hiv-care-lib/dqa-reports/dqa-reports/dqa-reports.component.ts index 72efb7770..884e59d82 100644 --- a/src/app/hiv-care-lib/dqa-reports/dqa-reports/dqa-reports.component.ts +++ b/src/app/hiv-care-lib/dqa-reports/dqa-reports/dqa-reports.component.ts @@ -31,12 +31,21 @@ export class DqaReportsComponent implements OnInit { public navigateToReport(reportName: any) { if (this.multipleLocation) { - this.router.navigate(['dqa-filter'], { - relativeTo: this.route, - queryParams: { - reportId: reportName.id + this.router.navigate( + [ + reportName.id === '1' + ? 'dqa-filter' + : reportName.id === '2' + ? 'case-surveillance' + : 'dqa-filter' + ], + { + relativeTo: this.route, + queryParams: { + reportId: reportName.id + } } - }); + ); } else { this.route.parent.parent.params.subscribe((params) => { const locationUuid = params['location_uuid']; diff --git a/src/app/hiv-care-lib/dqa-reports/dqa-reports/dqa-reports.json b/src/app/hiv-care-lib/dqa-reports/dqa-reports/dqa-reports.json index 68631da21..98f25c14c 100644 --- a/src/app/hiv-care-lib/dqa-reports/dqa-reports/dqa-reports.json +++ b/src/app/hiv-care-lib/dqa-reports/dqa-reports/dqa-reports.json @@ -3,5 +3,10 @@ "name": "Patient chart abstraction", "id": "1", "description": "DQA Chart Abstraction Report" + }, + { + "name": "Case Surveillance", + "id": "2", + "description": "Case Surveillance Report" } ] diff --git a/src/app/hiv-care-lib/hiv-care-lib.module.ts b/src/app/hiv-care-lib/hiv-care-lib.module.ts index a8c45d4f9..8ca9da8f5 100644 --- a/src/app/hiv-care-lib/hiv-care-lib.module.ts +++ b/src/app/hiv-care-lib/hiv-care-lib.module.ts @@ -148,6 +148,9 @@ import { AhdMonthlyReportPatientlistComponent } from './ahd-monthly-report/ahd-m import { PlhivNcdV2ReportBaseComponent } from './plhiv-ncd-v2-report/plhiv-ncd-v2-report-base/plhiv-ncd-v2-report-base.component'; import { PlhivNcdV2ReportPatientListComponent } from './plhiv-ncd-v2-report/plhiv-ncd-v2-report-patient-list/plhiv-ncd-v2-report-patient-list.component'; import { PlhivNcdV2ReportViewComponent } from './plhiv-ncd-v2-report/plhiv-ncd-v2-report-view/plhiv-ncd-v2-report-view.component'; +import { CaseSurveillanceBaseComponent } from './dqa-reports/case-surveillance/case-surveillance-base/case-surveillance-base.component'; +import { CaseSurveillancePatientListComponent } from './dqa-reports/case-surveillance/case-surveillance-patient-list/case-surveillance-patient-list.component'; +import { FacilityDashboardComponent } from './dqa-reports/case-surveillance/facility-dashboard/facility-dashboard.component'; @NgModule({ imports: [ @@ -348,7 +351,10 @@ import { PlhivNcdV2ReportViewComponent } from './plhiv-ncd-v2-report/plhiv-ncd-v AhdMonthlyReportPatientlistComponent, PlhivNcdV2ReportBaseComponent, PlhivNcdV2ReportPatientListComponent, - PlhivNcdV2ReportViewComponent + PlhivNcdV2ReportViewComponent, + CaseSurveillanceBaseComponent, + CaseSurveillancePatientListComponent, + FacilityDashboardComponent ], providers: [ MOHReportService, From 7d21da350683439f0e4ec99d4d882f8b0b95665c Mon Sep 17 00:00:00 2001 From: Denzel Kipkemoi Date: Mon, 1 Dec 2025 17:27:31 +0300 Subject: [PATCH 2/2] Create a Visit Called in HIV Program Called " Age Friendly Visit" --- .../visit-starter/visit-starter.component.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/app/patient-dashboard/common/visit/visit-starter/visit-starter.component.ts b/src/app/patient-dashboard/common/visit/visit-starter/visit-starter.component.ts index 22a3f8dee..a99c4ab69 100644 --- a/src/app/patient-dashboard/common/visit/visit-starter/visit-starter.component.ts +++ b/src/app/patient-dashboard/common/visit/visit-starter/visit-starter.component.ts @@ -24,6 +24,7 @@ import { CommunityGroupService } from '../../../../openmrs-api/community-group-r import { ProviderResourceService } from '../../../../openmrs-api/provider-resource.service'; import { Subscription } from 'rxjs'; import { ActivatedRoute } from '@angular/router'; +import { ToastrFunctionService } from 'src/app/shared/services/toastr-function.service'; @Component({ selector: 'app-visit-starter', @@ -123,7 +124,8 @@ export class VisitStarterComponent implements OnInit, OnDestroy { private communityGroupService: CommunityGroupService, private datePipe: DatePipe, private route: ActivatedRoute, - private providerResourceService: ProviderResourceService + private providerResourceService: ProviderResourceService, + private toastrService: ToastrFunctionService ) {} public ngOnInit() { @@ -192,6 +194,24 @@ export class VisitStarterComponent implements OnInit, OnDestroy { public startVisit(visitType) { this.selectedVisitType = visitType; + const hivAgeFriendlyVisitAllowedLocations = [ + '18c343eb-b353-462a-9139-b16606e6b6c2', // location test + '08feae7c-1352-11df-a1f1-0026b9348838', // module 1 + '08fec056-1352-11df-a1f1-0026b9348838', // module 2 + '08fec150-1352-11df-a1f1-0026b9348838' // module 3 + ]; + + if ( + this.selectedVisitType.uuid === '92e0e4da-5013-4a39-89ca-f7ee7e1e979a' && + !hivAgeFriendlyVisitAllowedLocations.includes(this.selectedLocation.value) + ) { + this.toastrService.showToastr( + 'error', + 'This visit is currently being piloted at only MTRH Modules 1-3 for patients aged over 50 years.', + '' + ); + return; + } if (visitType.groupVisit) { this._subscription.add( this.communityGroupMemberService