diff --git a/src/app/app-modules/core/components/app-header/app-header.component.html b/src/app/app-modules/core/components/app-header/app-header.component.html index 0104645f..e72389ba 100644 --- a/src/app/app-modules/core/components/app-header/app-header.component.html +++ b/src/app/app-modules/core/components/app-header/app-header.component.html @@ -159,6 +159,11 @@ {{ currentLanguageSet?.common?.report }} + diff --git a/src/app/app-modules/core/components/app-header/app-header.component.ts b/src/app/app-modules/core/components/app-header/app-header.component.ts index 298b3558..eb2ac9c4 100644 --- a/src/app/app-modules/core/components/app-header/app-header.component.ts +++ b/src/app/app-modules/core/components/app-header/app-header.component.ts @@ -80,6 +80,7 @@ export class AppHeaderComponent implements OnInit { isAuthenticated!: boolean; roles: any; helpURL: string = environment.licenseURL; + isMMUOfflineQRCode = environment.isMMUOfflineQRCode; filteredNavigation: any; isConnected = true; status!: any; diff --git a/src/app/app-modules/core/components/data-sync-login/data-sync-login.component.ts b/src/app/app-modules/core/components/data-sync-login/data-sync-login.component.ts index 269e6626..bf06e9cb 100644 --- a/src/app/app-modules/core/components/data-sync-login/data-sync-login.component.ts +++ b/src/app/app-modules/core/components/data-sync-login/data-sync-login.component.ts @@ -292,6 +292,23 @@ export class DataSyncLoginComponent implements OnInit, DoCheck { //added get datasync data on login to a new method getDataSyncMMU(res: any) { + const sessionUserID = this.sessionstorage.getItem('userID'); + const dataSyncUserID = res.data.userID; + + if ( + sessionUserID && + dataSyncUserID && + String(dataSyncUserID) !== String(sessionUserID) + ) { + this.showProgressBar = false; + sessionStorage.removeItem('serverKey'); + this.confirmationService.alert( + 'Sync user is not valid. Please login with the correct credentials.', + 'error' + ); + return; + } + const mmuService = res.data.previlegeObj.filter((item: any) => { return item.serviceName === 'MMU'; }); diff --git a/src/app/app-modules/data-sync/camp-hub-qr-code/camp-hub-qr-code.component.css b/src/app/app-modules/data-sync/camp-hub-qr-code/camp-hub-qr-code.component.css index c1b5fc20..f7bef2d4 100644 --- a/src/app/app-modules/data-sync/camp-hub-qr-code/camp-hub-qr-code.component.css +++ b/src/app/app-modules/data-sync/camp-hub-qr-code/camp-hub-qr-code.component.css @@ -45,6 +45,10 @@ min-width: 420px; } +.nikshay-tab-content { + padding-top: 12px; +} + .qr-subtitle { font-size: 13px; color: #555; diff --git a/src/app/app-modules/data-sync/camp-hub-qr-code/camp-hub-qr-code.component.html b/src/app/app-modules/data-sync/camp-hub-qr-code/camp-hub-qr-code.component.html index ce918604..f21cb3dc 100644 --- a/src/app/app-modules/data-sync/camp-hub-qr-code/camp-hub-qr-code.component.html +++ b/src/app/app-modules/data-sync/camp-hub-qr-code/camp-hub-qr-code.component.html @@ -28,72 +28,78 @@ -

Scan with the Stop TB Mobile app to connect over Wi-Fi.

+ + +
+

Scan with the Stop TB Mobile app to connect over Wi-Fi.

- -
- - Fetching Camp Hub server info… -
+ +
+ + Fetching Camp Hub server info… +
-
- info - - Camp Hub server unreachable — enter the server URL manually below. - - -
+
+ info + + Camp Hub server unreachable — enter the server URL manually below. + + +
-
- check_circle - Server URL fetched — review below and click Generate. -
+
+ check_circle + Server URL fetched — review below and click Generate. +
- -
- - Camp Hub Server URL - - Format: http://<network-ip>:<port>/ - - URL is required - - - Must start with http:// or https:// - - + + + + Camp Hub Server URL + + Format: http://<network-ip>:<port>/ + + URL is required + + + Must start with http:// or https:// + + -
- -
-
+
+ +
+ - -
-
- wifi - {{ generatedUrl }} -
+ +
+
+ wifi + {{ generatedUrl }} +
- Camp Hub QR Code + Camp Hub QR Code - -
+ +
+
+
+
diff --git a/src/app/app-modules/nikshay-sync/nikshay-sync-routing.module.ts b/src/app/app-modules/nikshay-sync/nikshay-sync-routing.module.ts new file mode 100644 index 00000000..5f52ac83 --- /dev/null +++ b/src/app/app-modules/nikshay-sync/nikshay-sync-routing.module.ts @@ -0,0 +1,38 @@ +/* + * 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 { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; +import { NikshaySyncComponent } from './nikshay-sync.component'; + +const routes: Routes = [ + { + path: '', + component: NikshaySyncComponent, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class NikshaySyncRoutingModule {} diff --git a/src/app/app-modules/nikshay-sync/nikshay-sync.component.css b/src/app/app-modules/nikshay-sync/nikshay-sync.component.css new file mode 100644 index 00000000..796b8951 --- /dev/null +++ b/src/app/app-modules/nikshay-sync/nikshay-sync.component.css @@ -0,0 +1,82 @@ +.nikshay-sync-page { + max-width: 640px; + margin: 32px auto; + padding: 24px 32px 16px; + background: #fff; + border-radius: 8px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15); +} + +.page-title { + font-size: 20px; + font-weight: 500; + color: #01579b; + margin-bottom: 16px; +} + +.nikshay-tab-content { + padding-top: 12px; +} + +.camp-info-chip { + display: inline-flex; + align-items: center; + gap: 6px; + background: #e3f2fd; + border-radius: 20px; + padding: 6px 16px; + font-size: 13px; + font-weight: 500; + color: #0b69b2; + margin-bottom: 8px; +} + +.qr-subtitle { + font-size: 13px; + color: #555; + margin-bottom: 8px; +} + +.detect-row { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: 4px; + font-size: 13px; + color: #555; + margin-bottom: 4px; +} + +.detect-row.warn { + background: #fff8e1; + color: #6d4c00; +} + +.detect-text { + flex: 1; + line-height: 1.4; +} + +.qr-result { + display: flex; + flex-direction: column; + align-items: center; + padding: 24px 0 8px; + gap: 16px; +} + +.url-icon { + font-size: 18px; + flex-shrink: 0; +} + +.url-text { + font-family: monospace; +} + +.btn-icon { + vertical-align: middle; + font-size: 18px; + margin-right: 4px; +} diff --git a/src/app/app-modules/nikshay-sync/nikshay-sync.component.html b/src/app/app-modules/nikshay-sync/nikshay-sync.component.html new file mode 100644 index 00000000..ff9a3b3e --- /dev/null +++ b/src/app/app-modules/nikshay-sync/nikshay-sync.component.html @@ -0,0 +1,168 @@ + + + + +
+

Nikshay Sync

+ + + +
+

+ Download this camp's beneficiaries as a CSV formatted for the Nikshay ID Generator. +

+ +
+ local_shipping + {{ campInfo.vanLabel }} · {{ campInfo.servicePointName }} +
+
+ info + + No active camp session found — select a van and service point first. + +
+ +
+ + Camp Date Range + + + + + MM/DD/YYYY – MM/DD/YYYY + + + + Both dates are required + + + +
+ +
+
+
+
+ + +
+

+ Upload the Nikshay ID Generator app's results CSV to write the generated Nikshay IDs + back onto this camp's beneficiaries. +

+ +
+ local_shipping + {{ campInfo.vanLabel }} · {{ campInfo.servicePointName }} +
+
+ info + + No active camp session found — select a van and service point first. + +
+ +
+
+ + + +
+ +
+ +
+
+ +
+

+ {{ lastImportSummary.updated }} Nikshay ID(s) updated · + {{ lastImportSummary.needsReview }} row(s) need review · + {{ lastImportSummary.failed }} failed + (of {{ lastImportSummary.csvRowCount }} row(s) in the file) +

+ +
+ Needs review: +
    +
  • + {{ row.firstName }} {{ row.middleLastName }} (benRegId {{ row.benRegId }}) — + {{ row.note }} +
  • +
+
+ +
+ Failed: +
    +
  • + {{ row.firstName }} {{ row.middleLastName }} (benRegId {{ row.benRegId }}) — + {{ row.note }} +
  • +
+
+
+
+
+
+
+ + diff --git a/src/app/app-modules/nikshay-sync/nikshay-sync.component.ts b/src/app/app-modules/nikshay-sync/nikshay-sync.component.ts new file mode 100644 index 00000000..1dd53774 --- /dev/null +++ b/src/app/app-modules/nikshay-sync/nikshay-sync.component.ts @@ -0,0 +1,269 @@ +/* + * 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 { Component, OnInit } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { FormBuilder, Validators } from '@angular/forms'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { saveAs } from 'file-saver'; +import { environment } from 'src/environments/environment'; +import { ConfirmationService } from '../core/services/confirmation.service'; + +interface CampInfo { + vanID: string; + vanLabel: string; + servicePointID: string; + servicePointName: string; +} + +interface ImportRowResult { + rowIndex: number; + benRegId: number | null; + firstName: string; + middleLastName: string; + status: string; + generatedId: string; + note: string; +} + +interface ImportSummary { + csvRowCount: number; + updated: number; + failed: number; + needsReview: number; + needsReviewRows: ImportRowResult[]; + failedRows: ImportRowResult[]; +} + +/** Keeps a single date-range download from spanning so many days that + * generating/transferring the CSV becomes slow enough to feel like it's + * hanging the browser tab. */ +const MAX_RANGE_DAYS = 90; + +@Component({ + selector: 'app-nikshay-sync', + templateUrl: './nikshay-sync.component.html', + styleUrls: ['./nikshay-sync.component.css'], +}) +export class NikshaySyncComponent implements OnInit { + // ── Download Beneficiaries (Nikshay CSV) tab ── + today = new Date(); + isDownloadingCsv = false; + campInfo: CampInfo | null = null; + + downloadForm = this.fb.group({ + fromDate: [null as Date | null, Validators.required], + toDate: [null as Date | null, Validators.required], + }); + + // ── Upload Nikshay Results tab ── + isUploadingCsv = false; + selectedResultsFile: File | null = null; + lastImportSummary: ImportSummary | null = null; + + constructor( + private fb: FormBuilder, + private http: HttpClient, + private sessionstorage: SessionStorageService, + private confirmationService: ConfirmationService + ) {} + + ngOnInit(): void { + this.loadCampInfo(); + } + + private loadCampInfo(): void { + try { + const serviceLine = JSON.parse( + this.sessionstorage.getItem('serviceLineDetails') ?? 'null' + ); + const servicePointID = this.sessionstorage.getItem('servicePointID'); + const servicePointName = this.sessionstorage.getItem('servicePointName'); + this.campInfo = + serviceLine?.vanID && servicePointID + ? { + vanID: serviceLine.vanID, + vanLabel: + serviceLine.vanNoAndType || + serviceLine.vehicalNo || + `Van ${serviceLine.vanID}`, + servicePointID, + servicePointName: servicePointName || `#${servicePointID}`, + } + : null; + } catch { + this.campInfo = null; + } + } + + downloadBeneficiariesCsv(): void { + if (this.downloadForm.invalid) { + this.downloadForm.markAllAsTouched(); + return; + } + if (!this.campInfo) { + this.confirmationService.alert( + 'No active camp session found — select a van and service point first.', + 'error' + ); + return; + } + + const fromDate = this.downloadForm.value.fromDate as Date; + const toDate = this.downloadForm.value.toDate as Date; + const rangeDays = + Math.round( + (toDate.getTime() - fromDate.getTime()) / (1000 * 60 * 60 * 24) + ) + 1; + + if (rangeDays < 1) { + this.confirmationService.alert( + 'The "From" date must be on or before the "To" date.', + 'error' + ); + return; + } + if (rangeDays > MAX_RANGE_DAYS) { + this.confirmationService.alert( + `Please choose a range of ${MAX_RANGE_DAYS} days or fewer so the download stays quick.`, + 'warning' + ); + return; + } + + const params = { + fromDate: this.formatDate(fromDate), + toDate: this.formatDate(toDate), + vanID: String(this.campInfo.vanID), + servicePointID: String(this.campInfo.servicePointID), + }; + + this.isDownloadingCsv = true; + this.http + .get(environment.nikshayBeneficiaryCsvUrl, { + params, + responseType: 'blob', + observe: 'response', + }) + .subscribe({ + next: (res: HttpResponse) => { + this.isDownloadingCsv = false; + const body = res.body; + if (!body || body.size === 0) { + this.confirmationService.alert( + 'No beneficiaries found for the selected date range.', + 'info' + ); + return; + } + const filename = this.extractFilename( + res.headers.get('content-disposition'), + `nikshay-beneficiaries-${params.fromDate}-to-${params.toDate}.csv` + ); + saveAs(body, filename); + }, + error: () => { + this.isDownloadingCsv = false; + this.confirmationService.alert( + 'Could not download the beneficiary CSV. Please try again.', + 'error' + ); + }, + }); + } + + onResultsFileSelected(event: Event): void { + const input = event.target as HTMLInputElement; + this.selectedResultsFile = input.files?.[0] ?? null; + this.lastImportSummary = null; + } + + uploadResultsCsv(): void { + if (!this.campInfo) { + this.confirmationService.alert( + 'No active camp session found — select a van and service point first.', + 'error' + ); + return; + } + if (!this.selectedResultsFile) { + this.confirmationService.alert( + 'Please choose the Nikshay results CSV file to upload.', + 'error' + ); + return; + } + + const formData = new FormData(); + formData.append('file', this.selectedResultsFile); + + const params = { + vanID: String(this.campInfo.vanID), + servicePointID: String(this.campInfo.servicePointID), + visitDate: this.formatDate(new Date()), + }; + + this.isUploadingCsv = true; + this.lastImportSummary = null; + this.http + .post(environment.nikshayImportResultsCsvUrl, formData, { + params, + }) + .subscribe({ + next: summary => { + this.isUploadingCsv = false; + this.lastImportSummary = summary; + const needsAttention = summary.failed > 0 || summary.needsReview > 0; + this.confirmationService.alert( + `Import complete — ${summary.updated} Nikshay ID(s) updated, ` + + `${summary.needsReview} row(s) need review, ${summary.failed} failed.`, + needsAttention ? 'warning' : 'info' + ); + }, + error: err => { + this.isUploadingCsv = false; + const message = + typeof err?.error === 'string' + ? err.error + : 'Could not import the results CSV. Please try again.'; + this.confirmationService.alert(message, 'error'); + }, + }); + } + + private formatDate(date: Date): string { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + return `${y}-${m}-${d}`; + } + + private extractFilename( + contentDisposition: string | null, + fallback: string + ): string { + const match = contentDisposition + ? /filename="?([^";]+)"?/i.exec(contentDisposition) + : null; + return match ? match[1] : fallback; + } +} diff --git a/src/app/app-modules/nikshay-sync/nikshay-sync.module.ts b/src/app/app-modules/nikshay-sync/nikshay-sync.module.ts new file mode 100644 index 00000000..fe6be6b9 --- /dev/null +++ b/src/app/app-modules/nikshay-sync/nikshay-sync.module.ts @@ -0,0 +1,49 @@ +/* + * 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule, FormsModule } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatButtonModule } from '@angular/material/button'; +import { MaterialModule } from '../core/material.module'; +import { CoreModule } from '../core/core.module'; +import { SharedModule } from '../core/components/shared/shared.module'; + +import { NikshaySyncRoutingModule } from './nikshay-sync-routing.module'; +import { NikshaySyncComponent } from './nikshay-sync.component'; + +@NgModule({ + imports: [ + CommonModule, + NikshaySyncRoutingModule, + CoreModule, + ReactiveFormsModule, + FormsModule, + MaterialModule, + MatFormFieldModule, + MatButtonModule, + SharedModule, + ], + declarations: [NikshaySyncComponent], +}) +export class NikshaySyncModule {} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 565bd019..557c57fe 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -109,6 +109,14 @@ const routes: Routes = [ module => module.DataSYNCModule ), }, + { + path: 'nikshay-sync', + canActivate: [AuthGuard], + loadChildren: () => + import('./app-modules/nikshay-sync/nikshay-sync.module').then( + module => module.NikshaySyncModule + ), + }, { path: 'feedback', loadChildren: () => diff --git a/src/environments/environment.ci.ts.template b/src/environments/environment.ci.ts.template index c1de5e62..de26b3ed 100644 --- a/src/environments/environment.ci.ts.template +++ b/src/environments/environment.ci.ts.template @@ -510,6 +510,8 @@ export const environment = { checkUsersignExistUrl: `${ADMIN_API}signature1/signexist/`, isEnableES: false, campHubConnectInfoAPI: `${COMMON_API}public/connect/info`, + nikshayBeneficiaryCsvUrl: `${MMU_API}stopTb/nikshay/exportBeneficiariesCsv`, + nikshayImportResultsCsvUrl: `${MMU_API}stopTb/nikshay/importResultsCsv`, elasticSearchUrl: `${MMU_API}registrar/quickSearchES`, advanceElasticSearchUrl: `${MMU_API}registrar/advancedSearchES`, diff --git a/src/environments/environment.dev.ts b/src/environments/environment.dev.ts index fc0b3ac8..d0b509d8 100644 --- a/src/environments/environment.dev.ts +++ b/src/environments/environment.dev.ts @@ -529,4 +529,6 @@ export const environment = { advanceElasticSearchUrl: `${MMU_API}registrar/advancedSearchES`, isSMSFeatureEnabled: false, campHubConnectInfoAPI: `${COMMON_API}public/connect/info`, + nikshayBeneficiaryCsvUrl: `${MMU_API}stopTb/nikshay/exportBeneficiariesCsv`, + nikshayImportResultsCsvUrl: `${MMU_API}stopTb/nikshay/importResultsCsv`, }; diff --git a/src/environments/environment.local.ts b/src/environments/environment.local.ts index 8173d6d6..29eeb346 100644 --- a/src/environments/environment.local.ts +++ b/src/environments/environment.local.ts @@ -527,4 +527,6 @@ export const environment = { advanceElasticSearchUrl: `${MMU_API}registrar/advancedSearchES`, isSMSFeatureEnabled: false, campHubConnectInfoAPI: `${COMMON_API}public/connect/info`, + nikshayBeneficiaryCsvUrl: `${MMU_API}stopTb/nikshay/exportBeneficiariesCsv`, + nikshayImportResultsCsvUrl: `${MMU_API}stopTb/nikshay/importResultsCsv`, }; diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index d4bda788..7794b0a3 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -524,4 +524,6 @@ export const environment = { advanceElasticSearchUrl: `${MMU_API}registrar/advancedSearchES`, isSMSFeatureEnabled: false, campHubConnectInfoAPI: `${COMMON_API}public/connect/info`, + nikshayBeneficiaryCsvUrl: `${MMU_API}stopTb/nikshay/exportBeneficiariesCsv`, + nikshayImportResultsCsvUrl: `${MMU_API}stopTb/nikshay/importResultsCsv`, }; diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index d4bda788..7794b0a3 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -524,4 +524,6 @@ export const environment = { advanceElasticSearchUrl: `${MMU_API}registrar/advancedSearchES`, isSMSFeatureEnabled: false, campHubConnectInfoAPI: `${COMMON_API}public/connect/info`, + nikshayBeneficiaryCsvUrl: `${MMU_API}stopTb/nikshay/exportBeneficiariesCsv`, + nikshayImportResultsCsvUrl: `${MMU_API}stopTb/nikshay/importResultsCsv`, };