diff --git a/src/app/clients/clients-view/clients-view.component.html b/src/app/clients/clients-view/clients-view.component.html index 92011c313..a98963d9c 100644 --- a/src/app/clients/clients-view/clients-view.component.html +++ b/src/app/clients/clients-view/clients-view.component.html @@ -13,7 +13,7 @@ - + + + + + + + + + diff --git a/src/app/clients/clients-view/custom-dialogs/capture-image-dialog/capture-image-dialog.component.scss b/src/app/clients/clients-view/custom-dialogs/capture-image-dialog/capture-image-dialog.component.scss new file mode 100644 index 000000000..767156536 --- /dev/null +++ b/src/app/clients/clients-view/custom-dialogs/capture-image-dialog/capture-image-dialog.component.scss @@ -0,0 +1,7 @@ +.fallback { + display: none; +} + +.canvas { + display: none; +} diff --git a/src/app/clients/clients-view/custom-dialogs/capture-image-dialog/capture-image-dialog.component.spec.ts b/src/app/clients/clients-view/custom-dialogs/capture-image-dialog/capture-image-dialog.component.spec.ts new file mode 100644 index 000000000..e802e2671 --- /dev/null +++ b/src/app/clients/clients-view/custom-dialogs/capture-image-dialog/capture-image-dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CaptureImageDialogComponent } from './capture-image-dialog.component'; + +describe('CaptureImageDialogComponent', () => { + let component: CaptureImageDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CaptureImageDialogComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CaptureImageDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/clients/clients-view/custom-dialogs/capture-image-dialog/capture-image-dialog.component.ts b/src/app/clients/clients-view/custom-dialogs/capture-image-dialog/capture-image-dialog.component.ts new file mode 100644 index 000000000..7e15e98c0 --- /dev/null +++ b/src/app/clients/clients-view/custom-dialogs/capture-image-dialog/capture-image-dialog.component.ts @@ -0,0 +1,108 @@ +/** Angular Imports */ +import { Component, ViewChild, ElementRef, AfterViewInit, Renderer2, OnDestroy } from '@angular/core'; +import { MatDialogRef } from '@angular/material'; + +/** + * Capture image dialog component + */ +@Component({ + selector: 'mifosx-capture-image-dialog', + templateUrl: './capture-image-dialog.component.html', + styleUrls: ['./capture-image-dialog.component.scss'] +}) +export class CaptureImageDialogComponent implements AfterViewInit, OnDestroy { + + /** Video element reference */ + @ViewChild('video') video: ElementRef; + /** Canvas element reference */ + @ViewChild('canvas') canvas: ElementRef; + /** Fallback element reference */ + @ViewChild('fallback') fallback: ElementRef; + /** Capture button element reference */ + @ViewChild('captureButton') captureButton: ElementRef; + + /** Toggles button states */ + isCaptured = false; + /** Client image file */ + clientImageDataURL: string; + + /** + * @param {MatDialogRef} dialogRef Mat Dialog Reference + * @param {Renderer2} renderer Template Renderer + */ + constructor(public dialogRef: MatDialogRef, + private renderer: Renderer2) {} + + ngAfterViewInit() { + this.startCamera(); + } + + ngOnDestroy() { + this.stopCamera(); + } + + /** + * Initializes camera video stream once user grants permission. + * Sets fallback if permission not granted. + * See https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices for configuration details. + */ + startCamera() { + if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { + navigator.mediaDevices.getUserMedia({ video: true }) + .then((stream: MediaStream) => { + this.renderer.setProperty(this.video.nativeElement, 'srcObject', stream); + this.video.nativeElement.play(); + }) + .catch((error: Error) => { + this.handleError(error); + }); + } else { + throw new Error('Cannot connect to camera'); + } + } + + /** + * Shows fallback message. + * @param {Error} error Error + */ + handleError(error: Error) { + this.renderer.removeStyle(this.fallback.nativeElement, 'display'); + const fallbackMessage = this.renderer.createText(`${error.name}: ${error.message}`); + this.renderer.appendChild(this.fallback.nativeElement, fallbackMessage); + this.renderer.setStyle(this.video.nativeElement, 'display', 'none'); + this.renderer.setProperty(this.captureButton, 'disabled', 'true'); + } + + /** + * Stops video stream and closes the camera. + * See https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack for `MediaStreamTrack` properties. + */ + stopCamera() { + const stream: MediaStream = this.video.nativeElement.srcObject; + if (stream) { + const videoStream: MediaStreamTrack = stream.getTracks()[0]; + videoStream.stop(); + } + this.renderer.setProperty(this.video.nativeElement, 'srcObject', null); + } + + /** + * Captures the image by drawing video state on canvas, then converts canvas state to data URL. + * See https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL for details. + */ + capture() { + this.isCaptured = true; + this.video.nativeElement.pause(); + this.canvas.nativeElement.getContext('2d').drawImage(this.video.nativeElement, 0, 0, 150, 150); + this.clientImageDataURL = this.canvas.nativeElement.toDataURL(); + } + + /** + * Allows user to capture image again. + */ + recapture() { + this.isCaptured = false; + this.video.nativeElement.play(); + } + +} diff --git a/src/app/clients/clients.module.ts b/src/app/clients/clients.module.ts index 660e9c645..bade27864 100644 --- a/src/app/clients/clients.module.ts +++ b/src/app/clients/clients.module.ts @@ -56,6 +56,7 @@ import { ClientFamilyMembersStepComponent } from './client-stepper/client-family import { ClientPreviewStepComponent } from './client-stepper/client-preview-step/client-preview-step.component'; import { ClientAddressStepComponent } from './client-stepper/client-address-step/client-address-step.component'; import { ClientFamilyMemberDialogComponent } from './client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component'; +import { CaptureImageDialogComponent } from './clients-view/custom-dialogs/capture-image-dialog/capture-image-dialog.component'; /** @@ -117,7 +118,8 @@ import { ClientFamilyMemberDialogComponent } from './client-stepper/client-famil ClientFamilyMembersStepComponent, ClientPreviewStepComponent, ClientAddressStepComponent, - ClientFamilyMemberDialogComponent + ClientFamilyMemberDialogComponent, + CaptureImageDialogComponent ], entryComponents: [ UploadDocumentDialogComponent, @@ -127,7 +129,8 @@ import { ClientFamilyMemberDialogComponent } from './client-stepper/client-famil UploadSignatureDialogComponent, DeleteSignatureDialogComponent, UploadImageDialogComponent, - ClientFamilyMemberDialogComponent + ClientFamilyMemberDialogComponent, + CaptureImageDialogComponent ], providers: [DatePipe] diff --git a/src/app/clients/clients.service.ts b/src/app/clients/clients.service.ts index 874751d4a..20a8b936c 100644 --- a/src/app/clients/clients.service.ts +++ b/src/app/clients/clients.service.ts @@ -166,6 +166,10 @@ export class ClientsService { return this.http.post(`/clients/${clientId}/images`, formData); } + uploadCapturedClientProfileImage(clientId: string, imageURL: string) { + return this.http.post(`/clients/${clientId}/images`, imageURL); + } + deleteClientProfileImage(clientId: string) { return this.http.delete(`/clients/${clientId}/images`); }