Summary Table
Categories |
Total Count |
PII |
0 |
URL |
0 |
DNS |
0 |
EKL |
0 |
IP |
0 |
PORT |
0 |
VsID |
0 |
CF |
0 |
AI |
0 |
VPD |
0 |
PL |
0 |
Other |
0 |
File Content
import {
Component,
OnInit,
OnDestroy,
Input,
Output,
EventEmitter,
OnChanges,
SimpleChanges,
Inject
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { DOCUMENT } from '@angular/platform-browser';
import { Subscription } from 'rxjs/Subscription';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Location } from '@angular/common';
// DomSanitizer and Sanitizer needed to inject xml files into the viewer iframe.
import { DomSanitizer } from '@angular/platform-browser';
import { AttachmentViewerService } from './attachment-viewer.service';
import { AuthenticationService } from '../../auth/auth.service';
import { AttachmentRequest } from './attachment-viewer.model';
import { ExportAsPDFRequest } from './export-as-pdf-request.model';
import { WindowRefService } from '../../window-ref.service';
import { Response } from '@angular/http';
import { environment } from '../../../environments/environment';
const LOGDBG = false; // log detailed debug messages
function logdbg(...args) {
if (LOGDBG) {
console.log('Debug ', ...args);
}
}
const LOGINF = false; // log information messages
function loginf(...args) {
if (LOGINF) {
console.log('Info ', ...args);
}
}
const LOGERR = true; // log critical error messages
function logerr(...args) {
if (LOGERR) {
console.log('Error ', ...args);
}
}
const isImageExtRx = /^BMP|GIF|JPE?G|PNG|TIFF?/i; // /i ignore case. Not supporting Targa yet
function isImageExt(ext: string) {
return isImageExtRx.test(ext);
}
@Component({
selector: 'app-attachment-viewer',
templateUrl: './attachment-viewer.component.html',
styleUrls: ['./attachment-viewer.component.scss']
})
export class AttachmentViewerComponent implements OnInit, OnChanges, OnDestroy {
// _tabbable = false;
// boolean to control whether to show/hide the attachment viewer.
@Input()
open = false;
// list of attachments returned from search results passed by search275 component.
@Input()
attachmentList: string[] = [];
// attachment that was clicked by end user.
selectedAttachment: string = null;
@Input()
attachKey: { attachIdLx: string; status: string } = null;
// notifies the parent component that the viewer needs to close;
@Output()
notify: EventEmitter<{}> = new EventEmitter();
@Input()
tabIdx = '0';
// get tabbable() {
// return this._tabbable;
// }
// @Output()
// tabbableChange = new EventEmitter(); // name should be @Input + 'Change'
// set tabbable(val: boolean) {
// // Emit value whenever tabbable changes
// if ((!this._tabbable && val) || (this._tabbable && !val)) {
// this.tabbableChange.emit(val);
// }
// this._tabbable = val;
// this.tabIndex = this.boolToTabindex(val); // tabIndex,_tabbable should stay in sync
// }
// tabIndex = this.boolToTabindex(this.tabbable);
@Output()
notifyArchive: EventEmitter<boolean> = new EventEmitter();
@Output()
notifyReActive: EventEmitter<boolean> = new EventEmitter();
cleanSelectedAttachment = null;
imgSrc = null;
xmlSrc = null;
UseMSIESave = true; // Initialized in constructor
queryAttachmentId: string = null;
arsViewer = false;
fppsViewer = false; // It's never set to true, so is it really needed?
// Seems like fppsViewer is a hold-over from previous code.
attachmentViewerLocation: string;
// Not sure why this is done, but at least do it in a single function
// old attachmentId still lurking, try to make the transition.
get locAttachIdLx() {
return this.attachmentViewerLocation === 'ATTACHMENT_VIEWER'
? this.queryAttachmentId // old way, should be updated in callers
: this.attachKey ? this.attachKey.attachIdLx : '';
}
get applicationLevel() {
return this.attachmentViewerLocation === 'ATTACHMENT_VIEWER'
? 'fpps'
: 'ars';
}
count = 0;
associateView = false;
fileExtension: string = null;
subscription: Subscription = new Subscription();
selectedAttachmentName: string = null;
modalView = false;
// backing store
// get modalView() {
// this.count++;
// return this._modalView;
// }
// set modalView(val: boolean) {
// this._modalView = val;
// this.tabIdx = val ? '-1' : '0';
// } // subscribers?
// need to replace the following with the message component.
tempMessage: string = null;
showMessage = false;
// exportFocus = false;
// exportPdfFocus = false;
// associateFocus = false;
// closeFocus = false;
form: FormGroup;
// following variables are for mapping the response from "Manual matching of unmatched 837 claim"
success: boolean;
failed: boolean;
errorDescription: string = null;
attachmentModified = false;
// <unUsed>
downloadDoc = false;
downloadRtf = false;
archiveSucessFlag = false;
archiveErrorFlag = false;
archiveSucessMsg = '';
archiveErrorMsg = '';
// </unUsed>
viewedXml = false;
userInfo;
// userInfo permissions cache. The user doesn't change, so it's safe to cache these values
exportPermission = false; // a permission we care about // set in ngOnInit()
editPermission = false; // a permission we care about // set in ngOnInit()
roleAdmin = false; // (was isAdmin) set in ngOnInit()
exportAsPDFShow = false; // This variable mixes permissions with UI visibility concerns. Is that OK?
// the ars vs. fpps idea still is unclear to me.
// get exportAsPDFShow() { return (this.arsViewer || this.fppsViewer) && this.exportPermission; }
// But, it's still intertwined with fileExtension, so no. Maybe later if it is separated.
// boolToTabindex(tab: boolean) {
// return tab ? '0' : '-1';
// } // translate tab to [tabindex] value (string)
// // I think tabI could return a number, but it is used as a string anyway.
constructor(
private sanitizer: DomSanitizer,
@Inject(DOCUMENT) private document,
private fb: FormBuilder,
private attachmentViewerService: AttachmentViewerService,
private authenticationService: AuthenticationService,
private location: Location,
private windowService: WindowRefService,
private route: ActivatedRoute
) {
this.UseMSIESave =
this.windowService.nativeWindow.navigator &&
this.windowService.nativeWindow.navigator.msSaveOrOpenBlob;
}
stringify(arg) {
return JSON.stringify(arg);
} // for use in html debugging
ngOnInit() {
if (this.route.snapshot.queryParams) {
this.route.queryParams.subscribe(params => {
const attachmentId = params['attachmentId'];
if (attachmentId) {
this.queryAttachmentId = attachmentId;
}
});
}
this.userInfo = this.authenticationService.getDecodedUserInfo();
// const additionalInformationPermission = this.userInfo.permissions.additionalInformation; // Not used
// "cache" the userInfo data into local variables for easy, terse, access
if (this.userInfo) {
const permissions = this.userInfo.permissions;
if (permissions && permissions.viewAttachment) {
this.exportPermission = permissions.viewAttachment.exportPermission;
this.editPermission = permissions.viewAttachment.editPermission;
}
// Search userRoles for "ADMIN".
// this.roleAdmin = (Array.isArray(this.userInfo.userRoles) && this.userInfo.userRoles.some(data => data === 'ADMIN') || false;
// Highly technical! Tricky to test. Use this instead.
this.roleAdmin = false; // Fail-safe if this.userInfo.userRoles is undefined or not an Array
if (Array.isArray(this.userInfo.userRoles)) {
this.roleAdmin = this.userInfo.userRoles.some(
data => data === 'ARS_ADMIN'
);
} else {
logerr('getDecodedUserInfo() returned no userRoles.');
// should not happen, but what if it does? Don't die. fail-safe: this.roleAdmin = false
this.roleAdmin = false;
}
} else {
logerr('getDecodedUserInfo() failed.');
}
this.formInit();
this.setLocation();
}
ngOnChanges(change: SimpleChanges) {
// if (change.selectedAttachment && change.selectedAttachment.currentValue) {
// this.cleanSelectedAttachment = this.sanitizer.bypassSecurityTrustResourceUrl(
// this.selectedAttachment
// );
// this.selectedAttachmentName = change.selectedAttachment.currentValue
// .split('/')
// .pop();
// }
if (change.open && change.open.currentValue) {
this.document.body.style.overflow = 'hidden';
// condition to prevent unwanted behavior when user clicks the back browser button.
this.setLocation();
this.onViewAttachment();
}
if (change.attachmentId && change.attachmentId.currentValue) {
// test code
}
}
setLocation() {
// just call this.location.path(false) once.
let locUrl = this.location.path(false);
// discard trailing query, if any
locUrl = this.location.path(false).split('?')[0]; // discard \?.*
// Use switch instead of nested, similar if statements. to consolidate
switch (locUrl) {
case '/ewv':
this.attachmentViewerLocation = 'EWV';
this.windowService.nativeWindow.history.pushState({}, 'ewv', '/ewv');
this.location.subscribe(() => {
this.closeViewer();
});
break;
case '/search275':
this.arsViewer = true; // boolean shows extra options for Ars Users.
this.attachmentViewerLocation = '275';
this.windowService.nativeWindow.history.pushState(
{},
'search275',
'/search275'
);
this.location.subscribe(() => {
this.closeViewer();
});
break;
case '/arsReports':
this.arsViewer = true;
this.attachmentViewerLocation = 'arsReports';
this.windowService.nativeWindow.history.pushState(
{},
'arsReports',
'/arsReports'
);
// Why no subscribe??
break;
case '/attachmentViewer':
this.open = true;
this.attachmentViewerLocation = 'ATTACHMENT_VIEWER'; // why so ugly?
this.onViewAttachment();
this.windowService.nativeWindow.history.pushState(
{},
'attachmentViewer',
'/attachmentViewer'
);
this.location.subscribe(() => {
this.closeViewer();
});
break;
default:
// ??
logerr('unexpected location.path in attachment-viewer');
break;
}
}
formInit() {
this.form = this.fb.group({
unmatched837Id: [
null,
[Validators.required, Validators.pattern('[0-9]*')]
]
});
}
onSubmit() {
// Must match backend expectations in Unmatched837AttachmentController::matchClaimAttachment
logdbg('this.form.value=', this.form.value);
// const request: Unmatched837Request = {
const request = {
...this.form.value,
attachIdLx: this.locAttachIdLx
};
this.subscription.add(
this.attachmentViewerService.match(request).subscribe(
// (data: Response) => { // but Property 'response' does not exist on type 'Response'
data => {
this.attachmentModified = true;
if (data.response) {
this.success = true;
this.associateView = false;
setTimeout(() => {
this.success = false;
}, 7500);
}
},
error => {
this.failed = true;
this.errorDescription = JSON.parse(error._body)['message'];
setTimeout(() => {
this.errorDescription = '';
this.failed = false;
}, 1000000000); // TODO: Fix messaging UX/UI
logdbg('attachmentViewer ', error);
}
)
);
}
closeViewer(): void {
if (this.attachmentViewerLocation === 'ATTACHMENT_VIEWER') {
this.windowService.nativeWindow.close();
}
if (this.fileExtension === 'XML') {
this.viewedXml = false;
this.attachmentViewerService
.cleanXmlTempFolder()
.subscribe(data => data.message);
}
const viewerNotify = {
open: false,
updateTable: this.attachmentModified
};
this.selectedAttachmentName = null;
this.fileExtension = null;
this.imgSrc = null;
this.xmlSrc = null;
this.cleanSelectedAttachment = null;
this.document.body.style.overflow = 'visible';
this.attachmentModified = false;
this.associateView = false;
this.form.reset();
this.reset();
this.notify.emit(viewerNotify);
this.showMessage = false;
this.success = false;
this.failed = false;
}
// setfileExtension(attachment): void {
// if (attachment) {
// this.selectedAttachmentName = attachment.split('/').pop();
// this.fileExtension = attachment
// .split('.')
// .pop()
// .toUpperCase();
// }
// }
viewAssociateForm(): void {
this.associateView = !this.associateView;
this.failed = false;
if (!this.associateView) {
this.form.controls['unmatched837Id'].setValue(null);
}
}
// modal related methods.
onArchiveView(): void {
this.modalView = true;
this.tabIdx = '-1'; // trigger
// continued in onArchive() after modal
// The weakest link
}
onArchive() {
this.modalView = false;
this.tabIdx = '0';
this.closeViewer();
const archiveRequest = { acceptedValues: [], status: true };
archiveRequest.acceptedValues.push(this.locAttachIdLx);
this.attachmentViewerService.archive(archiveRequest).subscribe(
data => {
this.notifyArchive.emit(true);
},
error => {
this.notifyArchive.emit(false);
}
);
}
reActivate() {
this.modalView = false;
this.closeViewer();
const archiveRequest = { acceptedValues: [], status: false };
archiveRequest.acceptedValues.push(this.locAttachIdLx);
this.attachmentViewerService.archive(archiveRequest).subscribe(
data => {
this.notifyReActive.emit(true);
},
error => {
this.notifyReActive.emit(false);
}
);
}
onModalClose(): void {
this.modalView = false;
this.tabIdx = '0';
}
// method to show the user a successful message once a path has been copied to the clipboard.
// clipBoardMessage(): void {
// this.tempMessage = 'The attachment path was copied to your clipboard.';
// this.showMessage = true;
// setTimeout(() => {
// this.showMessage = false;
// }, 7500);
// }
// focus(focus: string) {
// this.exportFocus = false;
// this.exportPdfFocus = false;
// this.associateFocus = false;
// this.closeFocus = false;
// this[focus] = true;
// }
// onClearFocus() {
// this.focus(undefined);
// }
reset() {
this.downloadDoc = false;
this.downloadRtf = false;
}
dataGetAttachmentName(data: Response) {
const name = data.headers.getAll('x-attachment-name'); // actually matches headers['x-attachment-name']
logdbg('x-attachment-name=', name);
return name ? name.toString() : '--missing name--';
}
// Currently unused
attachmentFileName(contentDisposition: string) {
const fileRx = /^attachment; *filename=(.*)$/;
if (!contentDisposition) {
return 'fishy.gif'; // ?? Need to do something.
}
const results = fileRx.exec(contentDisposition);
if (results && results.length > 1) {
// results[0]; is the whole match
return results[1]; // just the filename
}
return contentDisposition.toString();
}
fileTYP(name: string) {
// it should be a name, not a path. if filename='foo/bar.baz'
// call as fileTYP(filename.split('/').pop()) to skip any path, before calling fileTYP
return name
.split('.')
.pop()
.toUpperCase();
}
dataBlobAsType(data: Response): Blob {
logdbg('dataBlobAsType(data)=', data);
if (!data) {
logdbg('(!data)');
return null; // fail-thru
}
if (typeof data.blob !== 'function') {
logdbg('typeof data.blob=', typeof data.blob);
return null; // fail-thru
}
let dataBlob: Blob = data.blob();
logdbg('dataBlob=', dataBlob);
if (!data.headers) {
logdbg('(!data.headers)');
return dataBlob; // fail-thru
}
if (typeof data.headers.get !== 'function') {
logdbg('typeof data.headers.get=', typeof data.headers.get);
return dataBlob;
}
const contentType = data.headers.get('content-type').toString();
logdbg('contentType=', contentType);
logdbg('dataBlob.type=', dataBlob.type);
// on the off chance that the data.type is undefined/null yet contentType, is set, try to convert it anyway.
// if (!dataBlob.type) {
// return dataBlob;
// }
if (dataBlob && dataBlob.type !== contentType) {
logdbg(`convert data.blob() from ${dataBlob.type} to ${contentType}`);
dataBlob = new Blob([dataBlob], { type: contentType }); // convert blob to contentType
}
return dataBlob;
}
onViewAttachment() {
logdbg(
'onViewAttachment at attachmentViewerLocation',
this.attachmentViewerLocation
);
const attachmentRequest: AttachmentRequest = {
vhaName: this.userInfo.userName,
attachIdLx: this.locAttachIdLx,
applicationLevel: this.applicationLevel,
isDownload: false
};
logdbg('attachmentRequest', attachmentRequest);
this.attachmentViewerService // not the viewer, but the file getter
.viewAttachment(attachmentRequest) // not the view, but the file get
.subscribe((data: Response) => {
const filename = this.dataGetAttachmentName(data);
const name = filename.split('/').pop(); // drop any path part
this.fileExtension = this.fileTYP(name);
this.selectedAttachmentName = name;
const isImage = isImageExt(this.fileExtension);
// Now they want to exportAsPDF the XML, too! refactor.
this.exportAsPDFShow =
isImage &&
this.exportPermission &&
(this.arsViewer || this.fppsViewer);
let url = null;
if (this.fileExtension === 'XML') {
url = `${environment.nodeserver}/attachment/xml`; // call the service
} else {
logdbg('data=', data);
const dataBlob = this.dataBlobAsType(data);
logdbg('dataBlob.size=', dataBlob.size);
logdbg('dataBlob.type=', dataBlob.type);
url = this.windowService.nativeWindow.URL.createObjectURL(dataBlob);
}
logdbg('url=', url);
if (this.fileExtension === 'PDF') {
this.selectedAttachment = url; // no src
} else {
const src = this.sanitizer.bypassSecurityTrustResourceUrl(url);
// src is used in different places in the html, pick one
if (/^TXT/.test(this.fileExtension)) {
this.cleanSelectedAttachment = src;
} else if (/^TIFF?/.test(this.fileExtension)) {
// this.cleanSelectedAttachment = src;
this.imgSrc = src;
} else if (this.fileExtension === 'XML') {
this.xmlSrc = src;
} else if (isImage) {
this.imgSrc = src;
logdbg('imgSrc=', this.imgSrc);
} else {
logerr('onViewAttachment unhandled attachment type');
}
}
});
}
saveAs(blob: Blob, fileName: string) {
const url = this.windowService.nativeWindow.URL.createObjectURL(blob);
if (this.UseMSIESave) {
this.windowService.nativeWindow.navigator.msSaveOrOpenBlob(
blob,
fileName
);
} else {
// Build a DOM element and then click on it.
// Like window, global document shouldn't be used directly in Angular components.
const anchorElem = this.document.createElement('a');
// anchorElem.style = 'display: none';
anchorElem.href = url;
anchorElem.download = fileName;
this.document.body.appendChild(anchorElem);
anchorElem.click();
this.document.body.removeChild(anchorElem);
}
// On Edge, revokeObjectURL should be called only after
// a.click() has completed, at least on EdgeHTML 15.15048
// setTimeout(function(){
// The URL.revokeObjectURL() static method releases an existing object URL
// which was previously created by calling URL.createObjectURL().
// Call this method when you've finished using an object URL
// to let the browser know not to keep the reference to the file any longer.
// HOWEVER!
// Browsers will release object URLs automatically when the document is unloaded;
// however, for optimal performance and memory usage,
// if there are safe times when you can explicitly unload them, you should do so.
// this.windowService.nativeWindow.URL.revokeObjectURL(url);
// }, 15000); // Give the user some time to save the object
}
onExportAttachment() {
// Called from HTML (click)="onExportAttachment()"
// logdbg('onExportAttachment: exportPermission:', this.exportPermission);
if (!this.exportPermission) {
// abort!
// Should security be done in the service itself?
return null;
}
const attachmentRequest: AttachmentRequest = {
vhaName: this.userInfo.userName,
attachIdLx: this.locAttachIdLx, // naming conflict
applicationLevel: this.applicationLevel,
isDownload: true
};
// logdbg('onExportAttachment: attachmentRequest:', this.stringify(attachmentRequest));
return this.attachmentViewerService.exportAttachment(attachmentRequest).subscribe(
(data: Response) => {
// logdbg('onExportAttachment: data => data:', data);
const fileName = this.dataGetAttachmentName(data);
// this.fileExtension = this.fileTYP(fileName); // This belongs to viewAttachment, not exportAttachment
const blob = this.dataBlobAsType(data);
// const contentDisposition = data.headers.getAll('content-disposition').toString();
const attName = attachmentRequest.attachIdLx + '-' + fileName; // decorate the filename
// this.selectedAttachmentName = fileName; // This belongs to viewAttachment, not exportAttachment
this.saveAs(blob, attName);
},
error => {
logerr('onExportAttachment: error => error: ', error);
}
);
}
onExportAsPDF() {
if (!this.exportPermission) {
// abort!
// Should security be done in the service itself?
return;
}
const req: ExportAsPDFRequest = {
vhaName: this.userInfo.userName.toString(),
attachIdLx: this.locAttachIdLx
};
this.attachmentViewerService.exportAsPDF(req).subscribe(
(data: Response) => {
// Don't change this.fileExtension = this.fileTYP(fileName);
const fileName = this.dataGetAttachmentName(data); // looks like req.attachIdLx + '-' + fileName + '.pdf';
const blob = this.dataBlobAsType(data);
this.saveAs(blob, fileName);
},
error => {
logerr(error);
}
);
}
onRightClick(ev) {
ev.preventDefault();
}
ngOnDestroy() {
this.document.body.style.overflow = 'visible';
if (this.viewedXml) {
this.attachmentViewerService
.cleanXmlTempFolder()
.subscribe(data => data.message);
this.subscription.unsubscribe();
}
}
}