Skip to content

Commit

Permalink
feat(barcode): implement barcode component
Browse files Browse the repository at this point in the history
This commit implements a barcode component to make working with barcodes
a bit easier.  It replaces the cash barcode modal's custom logic with
generic logic to be tested.

Key features:
 1. If the input loses focus, the user is notified and a button appears
 to re-establish focus on the hidden input.
 2. Simple API - only a single callback is provided for the success
 case.
 3. Record agnostic.

Closes Third-Culture-Software#1529.
  • Loading branch information
jniles committed Mar 27, 2018
1 parent 52eca5c commit c2d6362
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 123 deletions.
16 changes: 11 additions & 5 deletions client/src/i18n/en/barcode.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{"BARCODE":{"SCAN":"Scan Invoice Barcode",
"AWAITING_INPUT":"Waiting for Input",
"AWAITING_HTTP":"Barcode Read! Looking up Invoice...",
"READ_SUCCESS":"Found Invoice",
"READ_ERROR":"Unreadable Barcode! Please enter the barcode manually."}}
{
"BARCODE" : {
"SCAN" : "Scan Document Barcode",
"AWAITING_INPUT" : "Waiting for Input",
"AWAITING_HTTP" : "Barcode Read! Looking up Record...",
"READ_SUCCESS" : "Found Record",
"READ_ERROR" : "Unreadable Barcode! Please enter the barcode manually.",
"LOST_FOCUS" : "The barcode input has lost focus. Please click \"Read Barcode\" to ready the barcode component.",
"RESET_BUTTON" : "Read Barcode"
}
}
16 changes: 11 additions & 5 deletions client/src/i18n/fr/barcode.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{"BARCODE":{"SCAN":"Scanner le Code à Barres",
"AWAITING_INPUT":"En Attente d'Entrée",
"AWAITING_HTTP":"Code à barres lu! Recherche de factures ....",
"READ_SUCCESS":"Facture Trouvée",
"READ_ERROR":"Code à barres illisible! Entrez le code à barres manuellement."}}
{
"BARCODE" : {
"SCAN" : "Scanner le Code à Barres",
"AWAITING_INPUT" : "En Attente d'Entrée",
"AWAITING_HTTP" : "Code à barres lu! Recherche l'enregistrement....",
"READ_SUCCESS" : "Trouvée",
"READ_ERROR" : "Code à barres illisible! Entrez le code à barres manuellement.",
"LOST_FOCUS" : "L'entrée du code à barres a perdu le focus. Veuillez cliquer sur \"Lire le code à barres\" pour préparer le composant de code à barres.",
"RESET_BUTTON" : "Lire le code à barres"
}
}
69 changes: 69 additions & 0 deletions client/src/js/components/bhBarcodeScanner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
angular.module('bhima.components')
.component('bhBarcodeScanner', {
templateUrl : '/modules/templates/bhBarcodeScanner.html',
controller : bhBarcodeScanner,
bindings : {
onScanCallback : '&',
},
});

bhBarcodeScanner.$inject = [
'$timeout', '$window', 'BarcodeService',
];

function bhBarcodeScanner($timeout, $window, Barcode) {
const $ctrl = this;

// steps in the search space
const steps = {
AWAIT_READ : 2,
AWAIT_HTTP : 4,
READ_SUCCESS : 8,
READ_ERROR : 16,
LOST_FOCUS : 32,
};

$ctrl.$onInit = () => {
angular.extend($ctrl, { steps });
$ctrl.currentStep = steps.AWAIT_READ;
$ctrl.setFocusOnHiddenInput();
};

const setFocusOnHiddenInput = () => {
// clear previous values
delete $ctrl.barcode;

// find and focus the input
const input = $window.document.getElementById('hidden-barcode-input');
input.focus();

// set view state correctly
$ctrl.isResetButtonVisible = false;
$ctrl.currentStep = steps.AWAIT_READ;
};

// wrap setFocusOnHiddenInput() in $timeout to trigger $digest
$ctrl.setFocusOnHiddenInput = () => {
$timeout(setFocusOnHiddenInput);
};

$ctrl.triggerBarcodeRead = () => {
$ctrl.currentStep = steps.AWAIT_HTTP;

Barcode.search($ctrl.barcode)
.then(record => {
$ctrl.currentStep = steps.READ_SUCCESS;
$ctrl.record = record;
$ctrl.onScanCallback({ record });
})
.catch(() => {
$ctrl.currentStep = steps.READ_ERROR;
$ctrl.isResetButtonVisible = true;
});
};

$ctrl.showResetButton = () => {
$ctrl.isResetButtonVisible = true;
$ctrl.currentStep = steps.LOST_FOCUS;
};
}
4 changes: 2 additions & 2 deletions client/src/js/services/BarcodeService.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
angular.module('bhima.services')
.service('BarcodeService', BarcodeService);

BarcodeService.$inject = [ '$http', 'util' ];
BarcodeService.$inject = ['$http', 'util'];

function BarcodeService($http, util) {
var service = this;
const service = this;

// TODO - barcode redirection
service.redirect = angular.noop;
Expand Down
2 changes: 1 addition & 1 deletion client/src/modules/cash/cash-form.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ function CashFormService(AppCache, Session, Patients, Exchange) {
CashForm.prototype.configure = function configure(config) {

if (config.patient) {
this.setPatient(config.invoices);
this.setPatient(config.patient);
}

if (config.description) {
Expand Down
100 changes: 23 additions & 77 deletions client/src/modules/cash/modals/barcode-scanner-modal.ctrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ angular.module('bhima.controllers')
.controller('CashBarcodeScannerModalController', CashBarController);

CashBarController.$inject = [
'$state', 'CashboxService', 'NotifyService', 'BarcodeService', 'PatientService',
'bhConstants', '$uibModalInstance', '$timeout', 'PatientInvoiceService', '$rootScope'
'$state', 'CashboxService', 'NotifyService', 'PatientService',
'bhConstants', '$uibModalInstance', '$timeout', 'PatientInvoiceService', '$rootScope',
];

/**
Expand All @@ -13,102 +13,48 @@ CashBarController.$inject = [
* This controller is responsible for scanning barcodes and the
* configuring the CashForm with the barcode
*/
function CashBarController($state, Cashboxes, Notify, Barcodes, Patients, bhConstants, Instance, $timeout, Invoices, RS) {
var vm = this;
var id = $state.params.id;
function CashBarController($state, Cashboxes, Notify, Patients, bhConstants, Instance, $timeout, Invoices, RS) {
const vm = this;
const { id } = $state.params;

var MODAL_CLOSE_TIMEOUT = 0;
const MODAL_CLOSE_TIMEOUT = 0;

vm.triggerBarcodeRead = triggerBarcodeRead;
vm.dismiss = dismiss;
vm.dismiss = () => Instance.dismiss();

vm.loading = true;

vm.INITIALIZED = 'initialized';
vm.LOADING = 'loading';
vm.READ_ERROR = 'read-error';
vm.READ_SUCCESS = 'found';

// the first step is initialized
vm.step = vm.INITIALIZED;

// determine if the input was a valid barcode
function isValidBarcode(input) {
return input.length >= bhConstants.barcodes.LENGTH;
}

function dismiss() {
Instance.dismiss();
}

// TODO(@jniles) potentially this should tell you if you are trying to read a
// cash payment instead of an invoice
// TODO(@jniles) potentially this should clear the input when the barcode
// is greater in length than 10.
// TODO(@jniles) this should be a component
function triggerBarcodeRead() {
if (isValidBarcode(vm.barcode)) {
searchForBarcode(vm.barcode);
} else {
vm.step = vm.READ_ERROR;
}
}
vm.onScanCallback = onScanCallback;

// send an HTTP request based on the barcode to get the invoice in question,
// then load the patient information in the background
function searchForBarcode(barcode) {

// set the loading step
vm.step = vm.LOADING;
function onScanCallback(invoice) {
let invoiceBalance;

Barcodes.search(barcode)
.then(function (invoice) {
vm.invoice = invoice;
return Invoices.balance(invoice.uuid);
return Invoices.balance(invoice.uuid)
.then(balance => {
invoiceBalance = balance;
return Patients.read(null, { debtor_uuid : invoice.debtor_uuid });
})
.then(function (balance) {
vm.balance = balance;
return Patients.read(null, { debtor_uuid : vm.invoice.debtor_uuid });
})
.then(function (patients) {

// de-structure search array
var patient = patients[0];

vm.patient = patient;
.then(patients => {
const [patient] = patients;

// emit the
// emit the configuration event
RS.$broadcast('cash:configure', {
invoices: [vm.balance],
patient: vm.patient,
description : vm.invoice.serviceName
patient,
invoices : [invoiceBalance],
description : invoice.serviceName,
});

vm.step = vm.READ_SUCCESS;

// close the modal after a timeout
$timeout(function () {
$timeout(() => {
Instance.close();
}, MODAL_CLOSE_TIMEOUT, false);
})
.catch(function (error) {
vm.step = vm.READ_ERROR;
})
.finally(function () {
toggleFlickerAnimation();
});
}

function toggleFlickerAnimation() {
vm.flicker = !vm.flicker;
.catch(Notify.handleError);
}

// fired on state startup
function startup() {
vm.flicker = true;

Cashboxes.read(id)
.then(function (cashbox) {
.then(cashbox => {
vm.cashbox = cashbox;
})
.catch(Notify.handleError);
Expand Down
36 changes: 3 additions & 33 deletions client/src/modules/cash/modals/barcode-scanner-modal.html
Original file line number Diff line number Diff line change
@@ -1,49 +1,19 @@
<form name="BarcodeForm" novalidate>

<div class="modal-header">
<ol class="headercrumb">
<li class="title" translate>BARCODE.SCAN</li>
</ol>
</div>

<div class="modal-body text-center" style="min-height:300px;">

<!-- state: awaiting barcode input -->
<h2 translate>BARCODE.SCAN</h2>

<h1>
<i class="fa fa-barcode fa-3x"></i>
</h1>

<p ng-class="{ 'animate-flicker' : BarcodeModalCtrl.flicker }">
<span class="text-muted" ng-if="BarcodeModalCtrl.step === BarcodeModalCtrl.INITIALIZED">
<span translate>BARCODE.AWAITING_INPUT</span>
</span>

<span class="text-primary" ng-if="BarcodeModalCtrl.step === BarcodeModalCtrl.LOADING">
<i class="fa fa-info-circle"></i>
<span translate>BARCODE.AWAITING_HTTP</span>
</span>

<span class="text-success" ng-if="BarcodeModalCtrl.step === BarcodeModalCtrl.READ_SUCCESS">
<i class="fa fa-check-circle-o"></i> <span translate>BARCODE.READ_SUCCESS</span> {{ ::BarcodeModalCtrl.invoice.reference }}
</span>

<span class="text-danger" ng-if="BarcodeModalCtrl.step === BarcodeModalCtrl.READ_ERROR">
<i class="fa fa-danger-sign"></i> <span translate>BARCODE.READ_ERROR</span>
</span>
</p>

<!-- hidden barcode input -->
<!-- TODO(@jniles): use ng-model-options to delay the $digest() loop and validate that a full barcode was inputted -->
<div style="width:0; overflow: hidden;">
<input ng-model="BarcodeModalCtrl.barcode" ng-change="BarcodeModalCtrl.triggerBarcodeRead()" bh-focus-on="true">
</div>
<bh-barcode-scanner on-scan-callback="BarcodeModalCtrl.onScanCallback(record)"></bh-barcode-scanner>
</div>

<div class="modal-footer text-right">
<button type="button" class="btn btn-default" ng-click="BarcodeModalCtrl.dismiss()">
<span translate>FORM.BUTTONS.CANCEL</span>
<button type="button" class="btn btn-default" ng-click="BarcodeModalCtrl.dismiss()" translate>
FORM.BUTTONS.CANCEL
</button>
</div>
</form>
37 changes: 37 additions & 0 deletions client/src/modules/templates/bhBarcodeScanner.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<div data-bh-barcode-scanner>

<!-- giant barcode to emphasize what this component is for -->
<h1 style="margin-bottom: 0;"><i class="fa fa-barcode fa-3x"></i></h1>

<p ng-class="{ 'animate-flicker' : $ctrl.currentStep === $ctrl.steps.AWAIT_READ }">
<span class="text-primary" ng-if="$ctrl.currentStep === $ctrl.steps.LOST_FOCUS">
<span translate>BARCODE.LOST_FOCUS</span>
</span>
<span class="text-muted" ng-if="$ctrl.currentStep === $ctrl.steps.AWAIT_READ">
<span translate>BARCODE.AWAITING_INPUT</span>
</span>

<span class="text-primary" ng-if="$ctrl.currentStep === $ctrl.steps.AWAIT_HTTP">
<i class="fa fa-info-circle"></i>
<span translate>BARCODE.AWAITING_HTTP</span>
</span>

<span class="text-success" ng-if="$ctrl.currentStep === $ctrl.steps.READ_SUCCESS">
<i class="fa fa-check-circle-o"></i> <span translate>BARCODE.READ_SUCCESS</span> {{ $ctrl.record.reference }}
</span>

<span class="text-danger" ng-if="$ctrl.currentStep === $ctrl.steps.READ_ERROR">
<i class="fa fa-danger-sign"></i> <span translate>BARCODE.READ_ERROR</span>
</span>
</p>

<!-- hidden barcode input -->
<div style="height:0; width:0; overflow: hidden;">
<input id="hidden-barcode-input" ng-model="$ctrl.barcode" ng-model-options="{ debounce : 150 }" ng-change="$ctrl.triggerBarcodeRead()" ng-blur="$ctrl.showResetButton()">
</div>

<!-- reset button to put focus on hidden input -->
<button class="btn btn-default" type="button" ng-show="$ctrl.isResetButtonVisible" ng-click="$ctrl.setFocusOnHiddenInput()" translate>
BARCODE.RESET_BUTTON
</button>
</div>

0 comments on commit c2d6362

Please sign in to comment.