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 : - '' 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 @@ @@ -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()">