From 697f43248304336d5c0b99af73e8666d8494245a Mon Sep 17 00:00:00 2001
From: Jonathan Niles
Date: Wed, 1 Jun 2016 14:12:17 +0100
Subject: [PATCH] feat(invoice): efficient change detection
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.
---
client/src/js/components/bhLoadingButton.js | 5 +--
client/src/js/services/Invoice.js | 34 ++++++++++++------
client/src/js/services/InvoiceItems.js | 6 ++--
.../js/services/PatientInvoiceItemService.js | 35 +++++++++++--------
client/src/js/services/util.js | 12 +++----
.../patient_invoice/patientInvoice.html | 16 ++++-----
.../patient_invoice/patientInvoice.js | 25 +++++++------
.../templates/grid/code.tmpl.html | 8 ++---
.../templates/grid/quantity.tmpl.html | 5 +--
.../templates/grid/status.tmpl.html | 20 +++++++++--
.../templates/grid/unit.tmpl.html | 5 +--
11 files changed, 104 insertions(+), 67 deletions(-)
diff --git a/client/src/js/components/bhLoadingButton.js b/client/src/js/components/bhLoadingButton.js
index 54cb387752..118a758a32 100644
--- a/client/src/js/components/bhLoadingButton.js
+++ b/client/src/js/components/bhLoadingButton.js
@@ -1,11 +1,12 @@
angular.module('bhima.components')
.component('bhLoadingButton', {
bindings : {
- loadingState : '<'
+ loadingState : '<',
+ disabled : '<'
},
transclude: true,
template :
- '' +
+ '' +
' {{ "FORM.INFOS.LOADING" | translate }} ' +
'{{ "FORM.BUTTONS.SUBMIT" | translate }} ' +
' '
diff --git a/client/src/js/services/Invoice.js b/client/src/js/services/Invoice.js
index 7769ea0fcc..5d10cb831d 100644
--- a/client/src/js/services/Invoice.js
+++ b/client/src/js/services/Invoice.js
@@ -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) {
@@ -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();
};
@@ -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();
};
/**
@@ -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
@@ -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;
}
diff --git a/client/src/js/services/InvoiceItems.js b/client/src/js/services/InvoiceItems.js
index fdbcbf5f9d..2d3d1ccbff 100644
--- a/client/src/js/services/InvoiceItems.js
+++ b/client/src/js/services/InvoiceItems.js
@@ -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) {
@@ -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;
@@ -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 {
diff --git a/client/src/js/services/PatientInvoiceItemService.js b/client/src/js/services/PatientInvoiceItemService.js
index 577c88927e..745baf52ea 100644
--- a/client/src/js/services/PatientInvoiceItemService.js
+++ b/client/src/js/services/PatientInvoiceItemService.js
@@ -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);
@@ -42,28 +49,23 @@ 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;
};
/**
@@ -71,6 +73,8 @@ function PatientInvoiceItemService(uuid) {
*
* @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;
@@ -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;
diff --git a/client/src/js/services/util.js b/client/src/js/services/util.js
index 96fce50c25..c753480f07 100644
--- a/client/src/js/services/util.js
+++ b/client/src/js/services/util.js
@@ -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');
@@ -136,6 +134,4 @@ function UtilService($filter) {
}
return cleaned;
};
-
-
}
diff --git a/client/src/partials/patient_invoice/patientInvoice.html b/client/src/partials/patient_invoice/patientInvoice.html
index 559bbe017f..b56c134daf 100644
--- a/client/src/partials/patient_invoice/patientInvoice.html
+++ b/client/src/partials/patient_invoice/patientInvoice.html
@@ -46,7 +46,7 @@
{{ "FORM.LABELS.TYPE" | translate }}
-
+
@@ -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">
- {{ "FORM.BUTTONS.RECOVER_ITEMS" | translate }}
+ {{ "FORM.BUTTONS.RECOVER_ITEMS" | translate }}
@@ -124,7 +124,7 @@
class="btn btn-default btn-sm"
ng-disabled="!PatientInvoiceCtrl.Invoice.recipient"
ng-click="PatientInvoiceCtrl.Invoice.rows.addItems(PatientInvoiceCtrl.itemIncrement)">
- {{ "FORM.BUTTONS.ADD" | translate }}
+ {{ "FORM.BUTTONS.ADD" | translate }}
-
+
{{ "FORM.INFOS.ITEMS_FULL" | translate }}
@@ -147,7 +147,7 @@
style="padding-top : 10px;"
class="text-info"
ng-if="!PatientInvoiceCtrl.Invoice.recipient">
-
+
{{ "FORM.INFOS.NO_RECIPIENT" | translate }}
@@ -217,7 +217,7 @@ {{ PatientInvoiceCtrl.Invoice.totals.billingServices | currency:PatientInvoi
{{ PatientInvoiceCtrl.Invoice.totals.subsidies | currency:PatientInvoiceCtrl.enterprise.currency_id }}
- {{ PatientInvoiceCtrl.Invoice.retotal() | currency:PatientInvoiceCtrl.enterprise.currency_id }}
+ {{ PatientInvoiceCtrl.Invoice.totals.grandTotal | currency:PatientInvoiceCtrl.enterprise.currency_id }}
@@ -236,12 +236,12 @@ {{ PatientInvoiceCtrl.Invoice.retotal() | currency:PatientInvoiceCtr
id="clear"
class="btn btn-default"
ng-click="PatientInvoiceCtrl.clear(detailsForm)">
- {{ "FORM.BUTTONS.CLEAR" | translate }}
+ {{ "FORM.BUTTONS.CLEAR" | translate }}
- {{ "FORM.INFOS.INVALID_ITEMS" | translate }}
+ {{ "FORM.INFOS.INVALID_ITEMS" | translate }}
diff --git a/client/src/partials/patient_invoice/patientInvoice.js b/client/src/partials/patient_invoice/patientInvoice.js
index ef8275e709..522e4e31e0 100644
--- a/client/src/partials/patient_invoice/patientInvoice.js
+++ b/client/src/partials/patient_invoice/patientInvoice.js
@@ -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;
@@ -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
@@ -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();
@@ -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];
@@ -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
@@ -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();
@@ -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()
diff --git a/client/src/partials/patient_invoice/templates/grid/code.tmpl.html b/client/src/partials/patient_invoice/templates/grid/code.tmpl.html
index 4548f11964..fb03e7aa1d 100644
--- a/client/src/partials/patient_invoice/templates/grid/code.tmpl.html
+++ b/client/src/partials/patient_invoice/templates/grid/code.tmpl.html
@@ -1,5 +1,5 @@
-
+
+ typeahead-on-select="grid.appScope.Invoice.configureItem(row.entity)">
-
+
+ ng-model-options="{ 'debounce' : { 'default' : 150, 'blur' : 0 }}"
+ ng-change="grid.appScope.handleChange()">
diff --git a/client/src/partials/patient_invoice/templates/grid/status.tmpl.html b/client/src/partials/patient_invoice/templates/grid/status.tmpl.html
index f79286e2f8..898bdc11ea 100644
--- a/client/src/partials/patient_invoice/templates/grid/status.tmpl.html
+++ b/client/src/partials/patient_invoice/templates/grid/status.tmpl.html
@@ -1,2 +1,18 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/partials/patient_invoice/templates/grid/unit.tmpl.html b/client/src/partials/patient_invoice/templates/grid/unit.tmpl.html
index 3412ddd240..8e89d4d43c 100644
--- a/client/src/partials/patient_invoice/templates/grid/unit.tmpl.html
+++ b/client/src/partials/patient_invoice/templates/grid/unit.tmpl.html
@@ -2,9 +2,10 @@
+ ng-model-options="{ 'debounce' : { 'default' : 150, 'blur' : 0 }}"
+ ng-change="grid.appScope.handleChange()">