Skip to content

Commit

Permalink
feat(invoice): efficient change detection
Browse files Browse the repository at this point in the history
This commit using ng-change to implement efficient change detection on
the patient invoice grid.  The previous implementation used angular's
$digest loop to calculate totals.

Additionally, each row now has three statuses:
 1. success: valid and initialised (has inventory item)
 2. warning: not initialised yet (no invenetory item)
 3. danger: invalid data

This should lead to a better user experience overall.
  • Loading branch information
jniles committed Jun 3, 2016
1 parent cc6ca3f commit 697f432
Show file tree
Hide file tree
Showing 11 changed files with 104 additions and 67 deletions.
5 changes: 3 additions & 2 deletions client/src/js/components/bhLoadingButton.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
angular.module('bhima.components')
.component('bhLoadingButton', {
bindings : {
loadingState : '<'
loadingState : '<',
disabled : '<'
},
transclude: true,
template :
'<button type="submit" class="btn btn-primary" ng-disabled="$ctrl.loadingState" data-method="submit">' +
'<button type="submit" class="btn btn-primary" ng-disabled="$ctrl.loadingState || $ctrl.disabled" data-method="submit">' +
'<span ng-show="$ctrl.loadingState"><span class="glyphicon glyphicon-refresh"></span> {{ "FORM.INFOS.LOADING" | translate }}</span>' +
'<span ng-hide="$ctrl.loadingState" ng-transclude>{{ "FORM.BUTTONS.SUBMIT" | translate }}</span>' +
'</button>'
Expand Down
34 changes: 24 additions & 10 deletions client/src/js/services/Invoice.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ InvoiceService.$inject = [
* This service provides helpers functions for the patient invoice controller.
* It is responsible for setting the form data for the invoice.
*
* @todo Discuss - currently all total values are force calculated by
* in the angular digest loop (from the angular template) - this vs. $watch
*
* @todo (required) Only the maximum of the bill should be subsidised
*/
function InvoiceService(InvoiceItems, Patients, PriceLists) {
Expand Down Expand Up @@ -70,8 +67,12 @@ function InvoiceService(InvoiceItems, Patients, PriceLists) {
this.totals = {
billingServices : 0,
subsidies : 0,
rows : 0
rows : 0,
grandTotal : 0
};

// trigger a totals digest
this.digest();
};


Expand All @@ -92,24 +93,28 @@ function InvoiceService(InvoiceItems, Patients, PriceLists) {
Patients.billingServices(patient.uuid)
.then(function (billingServices) {
invoice.billingServices = billingServices;
invoice.digest();
});

// load the subsidies and bind to the invoice
Patients.subsidies(patient.uuid)
.then(function (subsidies) {
invoice.subsidies = subsidies;
invoice.digest();
});

if (patient.price_list_uuid) {
PriceLists.read(patient.price_list_uuid)
.then(function (priceList) {
invoice.rows.setPriceList(priceList);
invoice.digest();
});
}

invoice.recipient = patient;
invoice.details.debtor_uuid = patient.debtor_uuid;
invoice.rows.addItems(1);
invoice.digest();
};

/**
Expand Down Expand Up @@ -138,14 +143,16 @@ function InvoiceService(InvoiceItems, Patients, PriceLists) {
* This method should be called anytime the values of the grid change,
* but otherwise, only on setPatient() completion.
*/
Invoice.prototype.retotal = function retotal() {
Invoice.prototype.digest = function digest() {
var invoice = this;
var totals = invoice.totals;
var grandTotal = 0;

// sum the rows in the invoice
// Invoice cost as modelled in the database does not factor in billing services
// or subsidies
var rowSum = invoice.rows.sum();
totals.rows = rowSum;
invoice.details.cost = rowSum;
grandTotal += rowSum;

// calculate the billing services total and increase the bill by that much
Expand All @@ -156,11 +163,18 @@ function InvoiceService(InvoiceItems, Patients, PriceLists) {
totals.subsidies = calculateSubsidies(invoice.subsidies, grandTotal);
grandTotal -= totals.subsidies;

// Invoice cost as modelled in the database does not factor in billing services
// or subsidies
invoice.details.cost = rowSum;
return grandTotal;
// bind the grandTotal
totals.grandTotal = grandTotal;
};

/**
* This method exists purely to intercept the change call from the grid.
*/
Invoice.prototype.configureItem = function configureItem(item) {
this.rows.configureItem(item);
this.digest();
};


return Invoice;
}
6 changes: 3 additions & 3 deletions client/src/js/services/InvoiceItems.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function InvoiceItemsService(InventoryService, Store, AppCache, PatientInvoiceIt
var priceReference = items.priceList.get(item.inventory_uuid);

if (angular.isDefined(priceReference)) {
item.priceListApplied = true;
item._hasPriceList = true;

if (priceReference.is_percentage) {

Expand All @@ -113,7 +113,7 @@ function InvoiceItemsService(InventoryService, Store, AppCache, PatientInvoiceIt
// filters out valid items
var invalidItems = items.rows.filter(function (row) {
row.validate();
return row.invalid;
return row._invalid;
});

return invalidItems;
Expand All @@ -124,7 +124,7 @@ function InvoiceItemsService(InventoryService, Store, AppCache, PatientInvoiceIt
row.validate();

// only sum valid rows
if (row.valid) {
if (row._valid) {
row.credit = (row.quantity * row.transaction_price);
return aggregate + row.credit;
} else {
Expand Down
35 changes: 20 additions & 15 deletions client/src/js/services/PatientInvoiceItemService.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,17 @@ function PatientInvoiceItemService(uuid) {
function PatientInvoiceItem(inventoryItem) {

// defaults
this.confirmed = false;
this.priceListApplied = false;
this.uuid = uuid();

// instance variable tracks if the row is valid
this._valid = false;

// instance variable tracks if the row has an inventory uuid
this._initialised = false;

// instance variable to track if a price list has altered the row's price.
this._hasPriceList = false;

// if inventoryItem exists, call the configure method right away
if (inventoryItem) {
this.configure(inventoryItem);
Expand All @@ -42,35 +49,32 @@ function PatientInvoiceItemService(uuid) {
* @description
* Validation for single PatientInvoiceItem. This is a prototype method since
* we are expecting to create potentially many items in an invoice.
*
* @returns {Boolean} - the validity of the current item. True is the item is
* valid, false if it is not.
*/
PatientInvoiceItem.prototype.validate = function validate() {
var item = this;

// ensure the numbers are valid in the invoice
var hasValidNumbers = angular.isNumber(item.quantity) &&
var hasValidNumbers =
angular.isNumber(item.quantity) &&
angular.isNumber(item.transaction_price) &&
item.quantity > 0 &&
item.transaction_price >= 0;

// item must be confirmed
var isConfirmed = item.confirmed;
// the item is only initialised if it has an inventory item
item._initialised = angular.isDefined(item.inventory_uuid);

// alias both valid and invalid for sy
item.valid = isConfirmed && hasValidNumbers;
item.invalid = !item.valid;

// return the boolean
return item.valid;
// alias both valid and invalid for easy reading
item._valid = item._initialised && hasValidNumbers;
item._invalid = !item._valid;
};

/**
* @method configure
*
* @description
* This method configures the PatientInvoiceItem with an inventory item.
*
* @param {Object} inventoryItem - an inventory item to copy into the view
*/
PatientInvoiceItem.prototype.configure = function configure(inventoryItem) {
this.quantity = 1;
Expand All @@ -79,7 +83,8 @@ function PatientInvoiceItemService(uuid) {
this.transaction_price = inventoryItem.price;
this.inventory_price = inventoryItem.price;
this.inventory_uuid = inventoryItem.uuid;
this.confirmed = true;

this.validate();
};

return PatientInvoiceItem;
Expand Down
12 changes: 4 additions & 8 deletions client/src/js/services/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,19 +108,17 @@ function UtilService($filter) {

// Define the maxLength By Value
service.length250 = 250;
service.length200 = 200;
service.length200 = 200;
service.length150 = 150;
service.length100 = 100;
service.length70 = 70;
service.length50 = 50;
service.length50 = 45;
service.length50 = 45;
service.length40 = 40;
service.length30 = 30;
service.length20 = 20;
service.length16 = 16;
service.length20 = 20;
service.length16 = 16;
service.length12 = 12;
service.length12 = 4;


// TODO This value is set in angular utilities - it could be configured on the enterprise
service.minimumDate = new Date('1900-01-01');
Expand All @@ -136,6 +134,4 @@ function UtilService($filter) {
}
return cleaned;
};


}
16 changes: 8 additions & 8 deletions client/src/partials/patient_invoice/patientInvoice.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@

<label class="control-label">
{{ "FORM.LABELS.TYPE" | translate }}
<span class="text-info glyphicon glyphicon-info-sign"></span>
<span class="text-info fa fa-info-circle"></span>
</label>

<!-- TODO distributable vs. non distributable sales should be designed/reviewed carefully -->
Expand Down Expand Up @@ -112,7 +112,7 @@
ng-class="{'btn-primary' : PatientInvoiceCtrl.Invoice.rows.cacheAvailable && PatientInvoiceCtrl.Invoice.recipient}"
ng-click="PatientInvoiceCtrl.Invoice.rows.recoverCache()"
ng-disabled="!PatientInvoiceCtrl.Invoice.rows.cacheAvailable || !PatientInvoiceCtrl.Invoice.recipient">
<span class="glyphicon glyphicon-floppy-open"></span> {{ "FORM.BUTTONS.RECOVER_ITEMS" | translate }}
<span class="fa fa-recycle"></span> {{ "FORM.BUTTONS.RECOVER_ITEMS" | translate }}
</button>
</div>

Expand All @@ -124,7 +124,7 @@
class="btn btn-default btn-sm"
ng-disabled="!PatientInvoiceCtrl.Invoice.recipient"
ng-click="PatientInvoiceCtrl.Invoice.rows.addItems(PatientInvoiceCtrl.itemIncrement)">
<span class="glyphicon glyphicon-plus-sign"></span> {{ "FORM.BUTTONS.ADD" | translate }}
<span class="fa fa-plus-circle"></span> {{ "FORM.BUTTONS.ADD" | translate }}
</button>
</span>
<input
Expand All @@ -139,15 +139,15 @@
style="padding-top : 10px;"
class="text-warning"
ng-if="PatientInvoiceCtrl.Invoice.rows.inventoryLoaded && PatientInvoiceCtrl.Invoice.rows.allAssigned()">
<span class="glyphicon glyphicon-info-sign"></span>
<span class="fa fa-info-circle"></span>
{{ "FORM.INFOS.ITEMS_FULL" | translate }}
</p>

<p
style="padding-top : 10px;"
class="text-info"
ng-if="!PatientInvoiceCtrl.Invoice.recipient">
<span class="glyphicon glyphicon-info-sign"></span>
<span class="fa fa-info-circle"></span>
{{ "FORM.INFOS.NO_RECIPIENT" | translate }}
</p>
</div>
Expand Down Expand Up @@ -217,7 +217,7 @@ <h4>{{ PatientInvoiceCtrl.Invoice.totals.billingServices | currency:PatientInvoi
<h4>{{ PatientInvoiceCtrl.Invoice.totals.subsidies | currency:PatientInvoiceCtrl.enterprise.currency_id }}</h4>

<!-- This is actually what causes all of the values referenced to be updated on every change -->
<h4><strong>{{ PatientInvoiceCtrl.Invoice.retotal() | currency:PatientInvoiceCtrl.enterprise.currency_id }}</strong></h4>
<h4><strong>{{ PatientInvoiceCtrl.Invoice.totals.grandTotal | currency:PatientInvoiceCtrl.enterprise.currency_id }}</strong></h4>
</div>
</div>
</div>
Expand All @@ -236,12 +236,12 @@ <h4><strong>{{ PatientInvoiceCtrl.Invoice.retotal() | currency:PatientInvoiceCtr
id="clear"
class="btn btn-default"
ng-click="PatientInvoiceCtrl.clear(detailsForm)">
<span class="glyphicon glyphicon-ban-circle"></span> {{ "FORM.BUTTONS.CLEAR" | translate }}
{{ "FORM.BUTTONS.CLEAR" | translate }}
</button>
<p
class="text-danger"
ng-if="PatientInvoiceCtrl.Invoice.rows.invalid && detailsForm.$submitted">
<span class="glyphicon glyphicon glyphicon-exclamation-sign"></span> {{ "FORM.INFOS.INVALID_ITEMS" | translate }}
<span class="fa fa-exclamation-circle"></span> {{ "FORM.INFOS.INVALID_ITEMS" | translate }}
</p>
</div>
</div>
Expand Down
25 changes: 14 additions & 11 deletions client/src/partials/patient_invoice/patientInvoice.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ PatientInvoiceController.$inject = [
*/
function PatientInvoiceController(Patients, PatientInvoices, Invoice, util, Services, Session, Dates, Receipts, Notify) {
var vm = this;
vm.Invoice = new Invoice();

// bind the enterprise to the enterprise currency
// bind the enterprise to get the enterprise currency id
vm.enterprise = Session.enterprise;
vm.Invoice = new Invoice();

// application constants
vm.maxLength = util.maxTextLength;
Expand All @@ -41,7 +41,7 @@ function PatientInvoiceController(Patients, PatientInvoices, Invoice, util, Serv
{ field: 'quantity', displayName: 'TABLE.COLUMNS.QUANTITY', headerCellFilter: 'translate', cellTemplate: 'partials/patient_invoice/templates/grid/quantity.tmpl.html' },
{ field: 'transaction_price', displayName: 'TABLE.COLUMNS.TRANSACTION_PRICE', headerCellFilter: 'translate', cellTemplate: 'partials/patient_invoice/templates/grid/unit.tmpl.html' },
{ field: 'amount', displayName: 'TABLE.COLUMNS.AMOUNT', headerCellFilter: 'translate', cellTemplate: 'partials/patient_invoice/templates/grid/amount.tmpl.html' },
{ field: 'actions', width : 25, cellTemplate: 'partials/patient_invoice/templates/grid/actions.tmpl.html' }
{ field: 'actions', width: 25, cellTemplate: 'partials/patient_invoice/templates/grid/actions.tmpl.html' }
],
onRegisterApi : exposeGridScroll,
data : vm.Invoice.rows.rows
Expand All @@ -60,14 +60,8 @@ function PatientInvoiceController(Patients, PatientInvoices, Invoice, util, Serv
});
}

// Invoice total and items are successfully sent and written to the server
// - Billing services are sent to the server but NOT recorded
// - Subsidies are sent to the server but NOT recorded
// - TODO the final value of a sale can only be determined after checking all
// billing services, subsidies and the cost of the sale
// invoice total and items are successfully sent and written to the server
function submit(detailsForm) {
var items = angular.copy(vm.Invoice.rows.rows);
console.log('vm.Invoice.rows', vm.Invoice.rows);

// update value for form validation
detailsForm.$setSubmitted();
Expand All @@ -82,7 +76,7 @@ function PatientInvoiceController(Patients, PatientInvoices, Invoice, util, Serv
var invalidItems = vm.Invoice.rows.validate();

if (invalidItems.length) {
Notify.danger('PATIENT_INVOICE.INVALID_INVOICE_ITEMS');
Notify.danger('PATIENT_INVOICE.INVALID_ITEMS');

var firstInvalidItem = invalidItems[0];

Expand All @@ -91,6 +85,9 @@ function PatientInvoiceController(Patients, PatientInvoices, Invoice, util, Serv
return;
}

// copy the rows for insertion
var items = angular.copy(vm.Invoice.rows.rows);

// invoice consists of
// 1. Invoice details
// 2. Invoice items
Expand All @@ -106,6 +103,11 @@ function PatientInvoiceController(Patients, PatientInvoices, Invoice, util, Serv
.catch(Notify.handleError);
}

// this function will be called whenever items change in the grid.
function handleChange() {
vm.Invoice.digest();
}

function handleCompleteInvoice(invoice) {
// vm.Invoice.rows.removeCache();
clear();
Expand Down Expand Up @@ -150,6 +152,7 @@ function PatientInvoiceController(Patients, PatientInvoices, Invoice, util, Serv
vm.setPatient = setPatient;
vm.submit = submit;
vm.clear = clear;
vm.handleChange = handleChange;

// read in services and bind to the view
Services.read()
Expand Down
Loading

0 comments on commit 697f432

Please sign in to comment.