Skip to content

Commit

Permalink
feat(invoice): invoice caching on submit
Browse files Browse the repository at this point in the history
This commit implements caching of patient invoices on submit.  When an
invoice is submitted, regardless of errors the form is cached in
AppCache.  Once the user successfully submits the form, the cache is
cleared.

The cache is only loaded once the user has chosen a patient.  The idea
is that a patient should not be cached, only the form data.

Finally, I've introduced an error flash on invalid grid rows.  To prompt
the error flash, one simply has to call `validate()` with the boolean
`true`.
  • Loading branch information
Jonathan Niles authored and jniles committed Oct 9, 2016
1 parent eb81365 commit 9d1eb21
Show file tree
Hide file tree
Showing 18 changed files with 170 additions and 69 deletions.
3 changes: 2 additions & 1 deletion client/src/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,8 @@ function constantConfig() {
maxTextLength : 1000
},
grid : {
ROW_HIGHLIGHT_FLAG : '_highlight'
ROW_HIGHLIGHT_FLAG : '_highlight',
ROW_ERROR_FLAG : '_error'
},
transactions : {
ROW_EDIT_FLAG : '_edit',
Expand Down
22 changes: 18 additions & 4 deletions client/src/js/services/PatientInvoiceForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ angular.module('bhima.services')

PatientInvoiceFormService.$inject = [
'PatientService', 'PriceListService', 'InventoryService', 'AppCache', 'Store',
'Pool', 'PatientInvoiceItemService'
'Pool', 'PatientInvoiceItemService', 'bhConstants'
];

/**
Expand All @@ -21,7 +21,9 @@ PatientInvoiceFormService.$inject = [
* specific debtors.
*
*/
function PatientInvoiceFormService(Patients, PriceLists, Inventory, AppCache, Store, Pool, PatientInvoiceItem) {
function PatientInvoiceFormService(Patients, PriceLists, Inventory, AppCache, Store, Pool, PatientInvoiceItem, Constants) {

var ROW_ERROR_FLAG = Constants.grid.ROW_ERROR_FLAG;

// Reduce method - assigns the current billing services charge to the billing
// service and adds to the running total
Expand Down Expand Up @@ -144,15 +146,20 @@ function PatientInvoiceFormService(Patients, PriceLists, Inventory, AppCache, St
* @description
* This method digests the invoice, then returns all invalid items in the
* invoice to be dealt with by the user.
*
* @param {Boolean} highlight - determines if the ROW_ERROR_FLAG should be set
* to highlight errors on the grid.
*/
PatientInvoiceForm.prototype.validate = function validate() {
PatientInvoiceForm.prototype.validate = function validate(highlight) {
this.digest();

// filters out valid items
var invalidItems = this.store.data.filter(function (row) {
row[ROW_ERROR_FLAG] = highlight ? row._invalid : false;
return row._invalid;
});


this._invalid = invalidItems.length > 0;
this._valid = !this._invalid;

Expand Down Expand Up @@ -371,20 +378,27 @@ function PatientInvoiceFormService(Patients, PriceLists, Inventory, AppCache, St

// set the details to the cached ones
this.details = cp.details;
this.details.date = new Date(this.details.date);

// set the patient
this.setPatient(cp.recipient);

// setPatient() adds an item. Remove it before configuring data
this.store.clear();
this.clear();

// loop through the cached items, configuring them
cp.items.forEach(function (cacheItem) {
var item = this.addItem();

item.inventory_uuid = cacheItem.inventory_uuid;
this.configureItem(item);

item.quantity = cacheItem.quantity;
item.transaction_price = cacheItem.transaction_price;
}.bind(this));

this.hasRecoveredCache = true;

// digest validation and totals
this.digest();
};
Expand Down
17 changes: 11 additions & 6 deletions client/src/js/services/PatientInvoiceItemService.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,18 @@ function PatientInvoiceItemService(uuid) {
*/
PatientInvoiceItem.prototype.configure = function configure(inventoryItem) {
this.quantity = 1;
this.code = inventoryItem.code;
this.description = inventoryItem.label;
this.transaction_price = inventoryItem.price;
this.inventory_price = inventoryItem.price;
this.inventory_uuid = inventoryItem.uuid;

// rest eh validation flags.
// when (empty) items are loaded from cache, not all of these codes are available.
// so we silently fail when there is an error.
try {
this.code = inventoryItem.code;
this.description = inventoryItem.label;
this.transaction_price = inventoryItem.price;
this.inventory_price = inventoryItem.price;
this.inventory_uuid = inventoryItem.uuid;
} catch (e) {}

// reset the validation flags.
this.validate();
};

Expand Down
11 changes: 11 additions & 0 deletions client/src/less/bhima-bootstrap.less
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,14 @@ input[type=number] {-moz-appearance: textfield;}
animation-name: highlight-flash;
animation-duration: 1.5s;
}

/* flash for error rows */
@keyframes error-flash {
from { background: @btn-danger-bg; }
to { background: transparent; }
}

.ui-grid-row .ui-grid-cell.row-error {
animation-name: error-flash;
animation-duration: 2s;
}
15 changes: 8 additions & 7 deletions client/src/partials/patient_invoice/patientInvoice.html
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,14 @@
<div class="col-xs-12">

<!-- "Recover Items" button -->
<div class="btn-group pull-right" role="group">
<div class="pull-right">
<button
class="btn btn-default"
ng-class="{'btn-primary' : PatientInvoiceCtrl.Invoice.hasCacheAvailable() && PatientInvoiceCtrl.Invoice.recipient }"
id="recover"
ng-class="{ 'btn-primary' : PatientInvoiceCtrl.Invoice.hasCacheAvailable() && PatientInvoiceCtrl.Invoice.recipient }"
ng-click="PatientInvoiceCtrl.Invoice.readCache()"
ng-disabled="!PatientInvoiceCtrl.Invoice.hasCacheAvailable() || !PatientInvoiceCtrl.Invoice.recipient">
<span class="fa fa-recycle"></span> {{ "FORM.BUTTONS.RECOVER_ITEMS" | translate }}
ng-disabled="!PatientInvoiceCtrl.Invoice.hasCacheAvailable() || !PatientInvoiceCtrl.Invoice.recipient || PatientInvoiceCtrl.Invoice.hasRecoveredCache || detailsForm.$submitted">
<i class="fa fa-recycle"></i> {{ "FORM.BUTTONS.RECOVER_ITEMS" | translate }}
</button>
</div>

Expand All @@ -120,15 +121,15 @@
<span class="input-group-btn">
<button
id="btn-add-rows"
class="btn btn-default btn-sm"
class="btn btn-default"
ng-disabled="!PatientInvoiceCtrl.Invoice.recipient"
ng-click="PatientInvoiceCtrl.addItems(PatientInvoiceCtrl.itemIncrement)">
<span class="fa fa-plus-circle"></span> {{ "FORM.BUTTONS.ADD" | translate }}
<i class="fa fa-plus-circle"></i> {{ "FORM.BUTTONS.ADD" | translate }}
</button>
</span>
<input
type="number"
class="form-control input-sm"
class="form-control"
ng-model="PatientInvoiceCtrl.itemIncrement"
style="max-width : 40px;"
ng-disabled="!PatientInvoiceCtrl.Invoice.recipient">
Expand Down
44 changes: 20 additions & 24 deletions client/src/partials/patient_invoice/patientInvoice.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ angular.module('bhima.controllers')

PatientInvoiceController.$inject = [
'PatientService', 'PatientInvoiceService', 'PatientInvoiceForm', 'util', 'ServiceService',
'SessionService', 'DateService', 'ReceiptModal', 'NotifyService'
'SessionService', 'DateService', 'ReceiptModal', 'NotifyService', 'bhConstants'
];

/**
Expand All @@ -16,12 +16,13 @@ PatientInvoiceController.$inject = [
* @todo (required) Invoice made outside of fiscal year error should be handled and shown to user
* @todo (requires) use a loading button for the form loading state.
*/
function PatientInvoiceController(Patients, PatientInvoices, PatientInvoiceForm, util, Services, Session, Dates, Receipts, Notify) {
function PatientInvoiceController(Patients, PatientInvoices, PatientInvoiceForm, util, Services, Session, Dates, Receipts, Notify, Constants) {
var vm = this;

// bind the enterprise to get the enterprise currency id
vm.enterprise = Session.enterprise;
vm.Invoice = new PatientInvoiceForm('PatientInvoiceForm');
vm.ROW_ERROR_FLAG = Constants.grid.ROW_ERROR_FLAG;

// application constants
vm.maxLength = util.maxTextLength;
Expand All @@ -31,17 +32,19 @@ function PatientInvoiceController(Patients, PatientInvoices, PatientInvoiceForm,

// read in services and bind to the view
Services.read()
.then(function (services) {
vm.services = services;
.then(function (services) {
vm.services = services;

// default to the first service
vm.Invoice.setService(services[0]);
});
// default to the first service
vm.Invoice.setService(services[0]);
})
.catch(Notify.handleError);

var gridOptions = {
appScopeProvider : vm,
enableSorting : false,
enableColumnMenus : false,
rowTemplate: 'partials/templates/grid/error.row.html',
columnDefs : [
{ field: 'status', width: 25, displayName : '', cellTemplate: 'partials/patient_invoice/templates/grid/status.tmpl.html' },
{ field: 'code', displayName: 'TABLE.COLUMNS.CODE', headerCellFilter: 'translate', cellTemplate: 'partials/patient_invoice/templates/grid/code.tmpl.html' },
Expand Down Expand Up @@ -71,18 +74,13 @@ function PatientInvoiceController(Patients, PatientInvoices, PatientInvoiceForm,

// invoice total and items are successfully sent and written to the server
function submit(detailsForm) {
vm.Invoice.writeCache();

// update value for form validation
detailsForm.$setSubmitted();

// if the form is invalid, return right away
if (detailsForm.$invalid) {
Notify.danger('FORM.ERRORS.RECORD_ERROR');
return;
}

// ask service items to validate themselves - if anything is returned it is invalid
var invalidItems = vm.Invoice.validate();
var invalidItems = vm.Invoice.validate(true);

if (invalidItems.length) {
Notify.danger('PATIENT_INVOICE.INVALID_ITEMS');
Expand All @@ -94,6 +92,12 @@ function PatientInvoiceController(Patients, PatientInvoices, PatientInvoiceForm,
return;
}

// if the form is invalid, return right away
if (detailsForm.$invalid) {
Notify.danger('FORM.ERRORS.RECORD_ERROR');
return;
}

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

Expand Down Expand Up @@ -125,18 +129,10 @@ function PatientInvoiceController(Patients, PatientInvoices, PatientInvoiceForm,
}

function handleCompleteInvoice(invoice) {
// vm.Invoice.rows.removeCache();
vm.Invoice.clearCache();

Receipts.invoice(invoice.uuid, true)
.then(function (result) {
clear();

// receipt closed fired
})
.catch(function (error) {

// receipt closed rejected
});
.then(function () { clear(); });
}

// register the patient search api
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<div style="padding : 5px;">
<a href="" ng-click="grid.appScope.Invoice.removeItem(row.entity)"><span class="fa fa-trash-o"></span></a>
<div class="ui-grid-cell-contents">
<a href="" ng-click="grid.appScope.Invoice.removeItem(row.entity)" class="text-danger">
<i class="fa fa-trash-o"></i>
</a>
</div>
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<div ng-hide="row.entity._valid"
<div
ng-hide="row.entity._valid"
ng-class="{
'bg-warning text-warning' : !row.entity._initialised,
'bg-danger text-danger' : row.entity._initialised
'bg-warning text-warning' : !row.entity._initialised,
'bg-danger text-danger' : row.entity._initialised
}"
style="padding: 5px;">
class="ui-grid-cell-contents">

<!-- warning state: invalid, but not initialised yet -->
<span ng-hide="row.entity._initialised" class="fa fa-circle-o"></span>
Expand All @@ -12,7 +13,7 @@
<span ng-show="row.entity._initialised" class="fa fa-times-circle"></span>
</div>

<div ng-show="row.entity._valid" class="bg-success" style="padding: 5px;">
<div ng-show="row.entity._valid" class="bg-success ui-grid-cell-contents">
<!-- success state: valid and initialised -->
<span class="text-success fa fa-check-circle"></span>
</div>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div style="padding : 5px;">
<input
min="0"
min="0.0001"
type="number"
ng-disabled="!row.entity._initialised"
class="form-control"
Expand Down
9 changes: 9 additions & 0 deletions client/src/partials/templates/grid/error.row.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div
ng-repeat="(colRenderIndex, col) in colContainer.renderedColumns track by col.uid"
class="ui-grid-cell"
ng-class="{
'ui-grid-row-header-cell': col.isRowHeader,
'row-error': row.entity[grid.appScope.ROW_ERROR_FLAG]
}"
ui-grid-cell>
</div>
2 changes: 1 addition & 1 deletion client/src/partials/templates/grid/highlight.row.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div
ng-repeat="(colRenderIndex, col) in colContainer.renderedColumns track by col.colDef.name"
ng-repeat="(colRenderIndex, col) in colContainer.renderedColumns track by col.uid"
class="ui-grid-cell"
ng-class="{
'ui-grid-row-header-cell': col.isRowHeader,
Expand Down
9 changes: 8 additions & 1 deletion server/controllers/finance/invoice/patientInvoice.create.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,20 @@ function processInvoice(invoiceUuid, invoice) {
if (invoice.debtor_uuid) {
invoice.debtor_uuid = db.bid(invoice.debtor_uuid);
}

invoice.uuid = invoiceUuid;

// cleanup details not directly tied to invoice
delete invoice.items;
delete invoice.billingServices;
delete invoice.subsidies;
return _.values(invoice);

const keys = [
'is_distributable', 'date', 'cost', 'description', 'service_id',
'debtor_uuid', 'project_id', 'user_id', 'uuid'
];

return keys.map(key => invoice[key]);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion server/models/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ DROP TABLE IF EXISTS `consumption_service`;
CREATE TABLE `consumption_service` (
`uuid` BINARY(16) NOT NULL,
`consumption_uuid` BINARY(16) NOT NULL,
`service_id` smallint(5) unsigned NOT NULL,
`service_id` SMALLINT(5) UNSIGNED NOT NULL,
PRIMARY KEY (`uuid`),
UNIQUE KEY `consumption_service_1` (`consumption_uuid`, `service_id`),
KEY `consumption_uuid` (`consumption_uuid`),
Expand Down
Loading

0 comments on commit 9d1eb21

Please sign in to comment.