AllcountJS case-study: the Point Of Sales

来源:转载

For the impatient

You could jump right tothe demo.

Introduction

It is unnecessary to introduce point of sales (POS) concept - it is everywhere in our life, not only in markets and places related to sales directly. Also it is tightly coupled with CRMs, inventory, warehousing, etc. With AllcountJS you can rapidly create all of this stuff. Let’s see how we can construct POS POC.

Considering model

First things first so let’s start with the essential thing – items which we are going to sale.

Item: { fields: {name: Fields.text("Name"),stock: Fields.integer("Stock").computed('sum(transactions.quantity)'),price: Fields.money("Price"),transactions: Fields.relation("Transactions", "Transaction", "item") }, referenceName: "name"}

The most interesting thing here is the stock field. It should becomputed according to quantities of this items involved into transactions. So that’s what we exactly wrote in our configuration: integer field stock is computed as sum of transactions’ quantity - but with parentheses, dots and quotes. And the “transactions” here is the field defined as “relation” with currently absent entity.Relation is defined by it’s name, related entity type and the field’s name of the related entity which reference to this item. So let’s add the Transaction entity type:

Transaction: {fields: {item: Fields.reference("Item", "Item"),order: Fields.reference("Order", "Order"),orderItem: Fields.reference("Order item", "OrderItem"),quantity: Fields.integer("Quantity")},showInGrid: ['item', 'order', 'quantity']}

As you can see, field “item” is referencing to the Item and maintaining the relation previously defined. So nothing special here except “showInGrid” option – it tells the view to show only listed fields by its identifiers. Next let’s define the central part of our POS:

Order: {fields: {number: Fields.integer("Order #"),date: Fields.date("Date"),total: Fields.money("Total").computed('sum(orderItems.finalPrice)'),orderItems: Fields.relation("Items", "OrderItem", "order")},beforeSave: function (Entity, Dates, Crud) {if (!Entity.date) {Entity.date = Dates.nowDate();}return Crud.crudFor('OrderCounter').find({}).then(function (last) {if (!Entity.number) {Entity.number = last[0].number;return Crud.crudFor('OrderCounter').updateEntity({id: last[0].id, number: last[0].number + 1});}})},beforeDelete: function (Entity, Crud, Q) {var crud = Crud.crudFor('OrderItem');return crud.find({filtering: {order: Entity.id}}).then(function (items) {return Q.all(items.map(function (i) {return crud.deleteEntity(i.id)}));});},referenceName: "number",views: {PointOfSale: {customView: 'pos'}}}

I called it “central” because this is the part where we define our main view: the “Point Of Sale” as a custom view located in file “pos.jade” which we will tackle with later. Also you’ve surely noticed “beforeSave” and “beforeDelete” functions. They are handlers which triggers when the appropriate event happens. More about such as functions you can learn fromour documentation. I’ll just mention that we need this functions here to update order’s counter when order is completed and clear up all related items from this order when it’s removed. You can also see relational and computed fields here. The latter is to show up total price of particular order and the former is the relation with entities which should be considered as items containing this order:

OrderItem: {fields: {order: Fields.reference("Order", "Order"),item: Fields.fixedReference("Item", "Item").required(),quantity: Fields.integer("Quantity").required(),finalPrice: Fields.money("Final price").readOnly().addToTotalRow()},showInGrid: ['item', 'quantity', 'finalPrice'],beforeSave: function (Crud, Entity) {return Crud.crudFor('Item').readEntity(Entity.item.id).then(function (item) {Entity.finalPrice = Entity.quantity * item.price;})},afterSave: function (Crud, Entity) {var crud = Crud.crudForEntityType('Transaction');return removeTransaction(Crud, Entity).then(function () {return crud.createEntity({order: Entity.order,orderItem: {id: Entity.id},item: Entity.item,quantity: Entity.quantity * -1})})},beforeDelete: function (Crud, Entity) {return removeTransaction(Crud, Entity);}}

Now you might noticed that every entity type which participate in the relational fields have its own “showInGrid” configuration. This is because usually we don’t want to show user the owner entity – he get himself there through it. But certainly you have an ability to customize this behaviour as you’d like. Here we have theCRUD-hooks too: we need to update total price according to ordered quantity before save this particular order’s item, we need to remove transaction and create a new one when editing of order’s items is successfully completed and we need to remove the transaction made with ordered item if it exists when this order’s item is going to be removed. Also we mark the “final price” field as a total-row-participant by calling the “add to total row” function on it. By the way, we worship of DRY principle , hence we’ve reused function removing transation:

function removeTransaction(Crud, Entity) {var crud = Crud.crudForEntityType('Transaction');return crud.find({filtering: {orderItem: Entity.id}}).then(function (transactions) {if (transactions.length) {return crud.deleteEntity(transactions[0].id);}});}

Just in case of the consecutive numbering of orders we’ve got singleton counter of orders:

OrderCounter: { fields: { number: Fields.integer("Counter") }} POS main view

Recently we’ve mentioned this view as an essential thing for such as applications. Remember yourself tackling horrible and overloaded user’s interfaces. And how happy you might be pushing just a few big colored buttons and getting your job done by it. That’s why we emphasize our customizability of UI. The bottom line is: if you have an outstanding UX, you can easily express it in the UI! And this is what we exactly going to do now – create a file called ‘pos.jade’ with the following content:

extends maininclude mixinsblock vars- var hasToolbar = falseblock contentdiv(ng-app='allcount', ng-controller='EntityViewController')+defaultList().container.screen-container(ng-cloak).row(ng-controller="PosController").col-md-8.items-bar.row.btn-toolbar(lc-list="'Item'", paging="{}").col-lg-4.col-md-6.col-xs-12(ng-repeat="item in items")button.btn.btn-lg.btn-block.btn-default(ng-click="addItem(item)")p {{item.name}}p {{(item.price / 100) | currency}}.container-fluidh1 Total: {{viewState.editForm.entity().total/100 | currency}}.row.btn-toolbar.col-md-4button.btn.btn-lg.btn-danger.btn-block(ng-click="deleteEntity()", ng-disabled="!viewState.formEntityId") Cancel.col-md-4(ng-hide='viewState.isFormEditing')+startFormEditingButton()(ng-disabled="!viewState.formEntityId").btn-block.btn-lg.col-md-4(ng-show='viewState.isFormEditing')+doneFormEditingButton()(ng-disabled="!viewState.formEntityId").btn-block.btn-lg.col-md-4button.btn.btn-lg.btn-success.btn-block(ng-click="viewState.mode = 'list'; viewState.formEntityId = undefined", ng-disabled="!viewState.formEntityId") Finish.col-md-4+defaultEditForm()(ng-show="true")+defaultFormTemplate()block js+entityJs()script.angular.module('allcount').controller('PosController', ['$scope', 'lcApi', '$q', function ($scope, lcApi, $q) {$scope.addItem = function (item) {var promise;if (!$scope.viewState.formEntityId) {promise = lcApi.createEntity({entityTypeId: 'Order'}, {}).then(function (orderId) {$scope.navigateTo(orderId)return orderId;})} else {promise = $q.when($scope.viewState.formEntityId);}promise.then(function (orderId) {return lcApi.findRange({entityTypeId: 'OrderItem'}, {filtering: {order: orderId}}).then(function (items) {var existingOrderItem = _.find(items, function (i) {return i.item.id === item.id;})return (existingOrderItem ?lcApi.updateEntity({entityTypeId: 'OrderItem'}, {id: existingOrderItem.id,quantity: 1 + existingOrderItem.quantity}) :lcApi.createEntity({entityTypeId: 'OrderItem'}, {order: {id: orderId},item: item,quantity: 1})).then(function () {return $scope.editForm.reloadEntity();})})})}}])style..items-bar .btn-block {margin-bottom: 10px;}

We are not going to stop on its content this time, but you can see it in action byrunning the demo!

Putting all things together

Alright, so far we’ve defined our nice and nifty view, decided about model and some business logic. If you followour blog you should be familiar with starting up AllcountJS. I’ll be brief with it:

Create directory somewhere on your computer (e.g. mkdir pos-example ) Create the app.js file inside it with the following content (as you can see we’ve added menus there and some sample data just for the sake of presentability): A.app({appName: "POS and inventory",appIcon: "calculator",onlyAuthenticated: true,menuItems: [{name: "Transactions",entityTypeId: "Transaction",icon: "send-o"}, {name: "Items",entityTypeId: "Item",icon: "cubes"}, {name: "Orders",entityTypeId: "Order",icon: "shopping-cart"},{name: "POS",entityTypeId: "PointOfSale",icon: "calculator"}],entities: function (Fields) {return {Transaction: {fields: {item: Fields.reference("Item", "Item"),order: Fields.reference("Order", "Order"),orderItem: Fields.reference("Order item", "OrderItem"),quantity: Fields.integer("Quantity")},showInGrid: ['item', 'order', 'quantity']},Item: {fields: {name: Fields.text("Name"),stock: Fields.integer("Stock").computed('sum(transactions.quantity)'),price: Fields.money("Price"),transactions: Fields.relation("Transactions", "Transaction", "item")},referenceName: "name"},Order: {fields: {number: Fields.integer("Order #"),date: Fields.date("Date"),total: Fields.money("Total").computed('sum(orderItems.finalPrice)'),orderItems: Fields.relation("Items", "OrderItem", "order")},beforeSave: function (Entity, Dates, Crud) {if (!Entity.date) {Entity.date = Dates.nowDate();}return Crud.crudFor('OrderCounter').find({}).then(function (last) {if (!Entity.number) {Entity.number = last[0].number;return Crud.crudFor('OrderCounter').updateEntity({id: last[0].id,number: last[0].number + 1});}})},beforeDelete: function (Entity, Crud, Q) {var crud = Crud.crudFor('OrderItem');return crud.find({filtering: {order: Entity.id}}).then(function (items) {return Q.all(items.map(function (i) {return crud.deleteEntity(i.id)}));});},referenceName: "number",views: {PointOfSale: {customView: 'pos'}}},OrderItem: {fields: {order: Fields.reference("Order", "Order"),item: Fields.fixedReference("Item", "Item").required(),quantity: Fields.integer("Quantity").required(),finalPrice: Fields.money("Final price").readOnly().addToTotalRow()},showInGrid: ['item', 'quantity', 'finalPrice'],beforeSave: function (Crud, Entity) {return Crud.crudFor('Item').readEntity(Entity.item.id).then(function (item) {Entity.finalPrice = Entity.quantity * item.price;})},afterSave: function (Crud, Entity) {var crud = Crud.crudForEntityType('Transaction');return removeTransaction(Crud, Entity).then(function () {return crud.createEntity({order: Entity.order,orderItem: {id: Entity.id},item: Entity.item,quantity: Entity.quantity * -1})})},beforeDelete: function (Crud, Entity) {return removeTransaction(Crud, Entity);}},OrderCounter: {fields: {number: Fields.integer("Counter")}},}},migrations: function (Migrations) {return [{name: "demo-records-1",operation: Migrations.insert("Item", [{id: "1", name: "Snickers", price: 299},{id: "2", name: "Coffee", price: 199},{id: "3", name: "Tea", price: 99}])},{name: "demo-records-2",operation: Migrations.insert("Transaction", [{id: "1", item: {id: "1"}, quantity: "50"},{id: "2", item: {id: "2"}, quantity: "100"},{id: "3", item: {id: "3"}, quantity: "200"}])},{name: "order-counter",operation: Migrations.insert("OrderCounter", [{id: "2", number: 1}])}]}});function removeTransaction(Crud, Entity) {var crud = Crud.crudForEntityType('Transaction');return crud.find({filtering: {orderItem: Entity.id}}).then(function (transactions) {if (transactions.length) {return crud.deleteEntity(transactions[0].id);}});} Place recently introduced pos.jade file right behind app.js Install allcountjs-cli if you haven’tdone it yet. Run the server by command allcountjs -c app.js (Note: you should already have Mongo DB service up and running) Afterword

Hope you enjoyed reading this case study. The feedback would be very appreciated! Don’t forget to follow our blog just not to miss something interesting! Also you are very welcome to our gitter chat – you can ask there whatever you want regarding every Node.js-related aspect or development in general.


分享给朋友:
您可能感兴趣的文章:
随机阅读: