/* Image Cropper — review, crop, clean and save identity photos. Identity comes from the YOUreka session; the only operator credential is the save-time OTP (required by a2loft). */ /* global Vue, axios, toastr, Croppa */ Vue.use(Croppa); (function () { 'use strict'; var REVIEW_OPTIONS = [ 'IMAGE: Not in the midline of the photo frame (aligned to the left or right)', 'IMAGE: Taken too close-up (Only head is visible, shoulders barely visible)', 'IMAGE: Blurry ', 'IMAGE: Improper lighting ', 'IMAGE: Not coloured.', 'IMAGE: Scanned photo', 'IMAGE: Has no adequate space to allow for proper cropping ', 'IMAGE: Not taken on a recommended background.', 'IMAGE: Photo is a selfie', 'DRESSING: Unacceptable dress. ', 'HEAD: Skewed (leaning away from the midline - vertically or horizontally)', 'EYES: Not looking into the camera (looking downward, upward, or sideward)', 'EYES: Not properly opened', 'EARS: Not visible ', 'EARS: Both ears are not equally visible (face not aligned central)', 'FACIAL EXPRESSION: NOT neutral (subject has a facial expression)', 'GLASSES: Subject is wearing glasses ', 'JUST WRING: Goes against almost all acceptable passport criteria.', 'Others not specified.', ]; var app = new Vue({ el: '#app', data: { loading: true, session: null, message: { message: { caption: null, file_url: null, domain: '' }, }, agent: null, process: null, reviewed: false, accepted: false, cleanedImage: null, requiresCleaning: '', otp: '', reviewOptions: REVIEW_OPTIONS.map(function (name, index) { return { index: index + 1, name: name }; }), selectedReviewOptions: [], otherReviewOption: '', busy: { load: false, clean: false, review: false, save: false }, myCroppa: {}, }, computed: { initials: function () { var n = (this.session && (this.session.firstName || this.session.phone)) || '?'; return n.slice(0, 2).toUpperCase(); }, canReview: function () { if (this.busy.review) return false; if (this.accepted) return true; return this.selectedReviewOptions.length > 0 || this.otherReviewOption.length > 0; }, canSave: function () { return !this.busy.save && this.otp.length > 0 && this.requiresCleaning !== ''; }, }, created: function () { var self = this; axios.get('/auth/session').then(function (r) { self.session = r.data; self.loading = false; }).catch(function (err) { if (err.response && err.response.status === 401) { window.location.href = '/auth/login'; return; } self.loading = false; toastr.error('Could not load your session. Please refresh the page.', 'Oops!'); }); }, methods: { reset: function () { this.message = { message: { caption: null, file_url: null, domain: '' } }; this.agent = null; this.process = null; this.reviewed = false; this.accepted = false; this.cleanedImage = null; this.selectedReviewOptions = []; this.otherReviewOption = ''; this.requiresCleaning = ''; this.otp = ''; }, loadPicture: async function (mode) { var self = this; this.busy.load = true; try { var uploadId = null; if (mode === 'by-id') { uploadId = prompt('Enter picture upload ID'); if (!uploadId) return; } var { data } = await axios.get( uploadId ? '/cropper/picture/load/' + encodeURIComponent(uploadId) : '/cropper/picture/next' ); if (!data.ok) { this.reset(); // nothing (more) to review — back to the load state toastr.error(data.message, 'Oops!'); return; } this.message = data.data; var image = await axios.get( '/cropper/picture/' + encodeURIComponent(this.message.message.file_name.replace(/\//gi, '_')) ); if (!image.data.ok) { this.reset(); toastr.error("Couldn't load the attached image. Please try again.", 'Oops!'); return; } this.message.message.file_url = image.data.data; this.cleanedImage = null; this.agent = null; this.process = this.message.message.process || null; if (this.message.message.upload_id) { var review = await axios.get( '/cropper/picture/review/' + encodeURIComponent(this.message.message.upload_id) ); this.agent = review.data.ok ? review.data.data : null; } } catch ({ message }) { toastr.error(message, 'Operation Failed'); } finally { this.reviewed = false; this.accepted = false; this.selectedReviewOptions = []; this.otherReviewOption = ''; this.requiresCleaning = ''; this.otp = ''; this.busy.load = false; } }, cleanImage: async function () { var self = this; this.busy.clean = true; try { var dataUrl = this.myCroppa.generateDataUrl('image/jpeg'); var response = await fetch('/cropper/api/removebg', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image_file_b64: dataUrl.split(',')[1] }), }); if (!response.ok) { var err = await response.json().catch(function () { return {}; }); throw new Error(err.message || 'Failed to clean image.'); } var contentType = response.headers.get('content-type') || ''; if (contentType.indexOf('image/') !== 0) { throw new Error('Image cleanup service returned an invalid response.'); } var blob = await response.blob(); var fr = new FileReader(); fr.onload = function () { self.cleanedImage = fr.result; }; fr.readAsDataURL(blob); } catch ({ message }) { toastr.error(message, 'Image Cleanup Failed'); } finally { this.busy.clean = false; } }, reviewPicture: async function () { var self = this; if (!this.canReview) return; if (!this.accepted && this.selectedReviewOptions.length === 0 && !this.otherReviewOption) { toastr.error('Please indicate why this image is not good enough.', 'Operation Failed'); return; } this.busy.review = true; var options = [...this.selectedReviewOptions, this.otherReviewOption].filter(function (o) { return o && o.length > 0; }); var reviewMessage = this.accepted ? null : options.join('\n\n'); try { var hasProcess = !!this.message.message.process; var data = {}; if (hasProcess) { var { domain, reference } = this.message.message.process.variables; data.process = { correlationKey: domain + '-' + reference, name: 'PictureReviewed', variables: { ok: this.accepted, reviewRejectMessage: reviewMessage }, }; } else { data.raw = { phone: this.message.message.caption, reviewRejectMessage: reviewMessage }; } var { data: result } = await axios.post('/cropper/picture/review', { deleteFromQueue: true, message: this.message, mode: hasProcess ? 'process' : 'raw', ...data, }); if (!result.ok) { toastr.error(result.message, 'Operation Failed'); return; } if (reviewMessage) { toastr.success('Profile owner has been notified of the review.', 'Review Sent'); } this.reset(); } catch ({ message }) { toastr.error(message, 'Operation Failed'); } finally { this.busy.review = false; } }, removeFromQueue: async function () { var self = this; if (!confirm('Are you sure you want to dequeue?')) return; this.busy.review = true; try { var { data } = await axios.post('/cropper/dequeue', { message: this.message }); if (!data.ok) { toastr.error(data.message, 'Not Removed'); return; } toastr.success(data.message, 'Removed!'); this.reset(); } catch ({ message }) { toastr.error(message, 'Not Removed'); } finally { this.busy.review = false; } }, saveChanges: async function () { var self = this; if (!this.canSave) return; this.busy.save = true; try { var { data } = await axios.post( '/cropper/profile/picture?otp=' + encodeURIComponent(this.otp), { pictureDataUrl: this.cleanedImage || this.myCroppa.generateDataUrl('image/jpeg'), phone: this.message.message.caption, message: this.message, requiresCleaning: this.requiresCleaning !== 'No', } ); if (!data.ok) { toastr.error(data.message, 'Not Saved'); return; } toastr.success('Picture saved for ' + this.message.message.caption, 'Saved'); this.reset(); } catch ({ message }) { toastr.error(message, 'Not Saved'); } finally { this.busy.save = false; } }, }, }); })();