Building simple Point of Sale system with Node.js & React.js

Building real time applications can be exciting, the idea that pages can be updated without reloading has always been of interest to me.
For the this tutorial we will be creating a real-time point of sale system using node.js and react.js

Get the source code and see demo here

This tutorial will comprise of three parts:

Part 1 (BackEnd)

  1. Frameworks description
  2. Building the Node app from scratch
  3. Testing with Postman

Part 2 (FrontEnd)
1.Creating a Template React app.
2.Creating Routes and Views with Code Description.

I recommend using the Vscode Editor for this tutorial.

Frameworks Description and Installation

Below are the libraries and frameworks we will be using:

nedb: NeDB is much like SQLite in that it is a smaller, embeddable version of a much larger database system.NeDB is a smaller NoSQL datastore that mimics MongoDB.

socket.io:Socket.IO enables real-time bidirectional event-based communication.It works on every platform, browser or device, focusing equally on reliability and speed.
express: Express is a Fast, unopinionated, minimalist web framework for Node.js. express features will enable us to create our web server.
async
nodemon: Nodemon checks for changes in your source and automatically restart your server.
body-parser: body-parser extract the entire body portion of an incoming request stream and exposes it on req.body .
http: Http allows Node.js to transfer data over the Hyper Text Transfer Protocol (HTTP).

Let’s continue by creating the backend with node.js, I will assume you have node and npm installed.

** Building the Node app from scratch**

For this tutorial we are going to create the Node app (express app) from scratch. it can also be done automatically using the ejs template.

Create a directory via your Command Line Interface (CLI) named real-time-pos-system

mkdir real-rime-pos-system

Access the folder via CLI thus:

cd real-time-pos-system

Inside your real-time-pos-system folder create new folder named server from CLI

mkdir server

Let’s install our dependencies:

npm init

Press enter button for the following asked questions:

package name: (server) Press Enter
version: (1.0.0) Press Enter
description: Node.js app that connect the react-pos app to the Database
entry point:(index.js) Press Enter
test command: Press Enter
git repository: Press Enter
keywords: Press Enter
author: Enter Your Name
license: (ISC) MIT

you will be shown the following message:

{
"name": "server"
version: "1.0.0"
"description": "Node.js app that connect the react-pos app to the Database
"main" : "index.js",
"scripts": {
test": "echo "Error: no test specified specified" && exit 1"
},
"author": "Your Name",
"license": "MIT"
}
Is this ok?(yes) yes

Install the following dependencies:

npm install express --save

npm install http --save

npm install nodemon --save

npm install nedb --save

Create a file named index.js in your real-time-pos-system folder using your Editor.

index.js is the entry point for our node app, as you can see it is located in the root of our app.

insert the following code in your index.js file

var express = require("express"),
http = require("http"),
port = 80,
app = require("express")(),
server = http.createServer(app),
bodyParser = require("body-parser"),
io = require("socket.io")(server),
liveCart;
console.log("Real time POS running");
console.log("Server started");
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.all("/*", function(req, res, next) {
// CORS headers
res.header("Access-Control-Allow-Origin", "*"); // restrict it to the required domain
res.header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS");
// Set custom headers for CORS
res.header(
"Access-Control-Allow-Headers",
"Content-type,Accept,X-Access-Token,X-Key"
);
if (req.method == "OPTIONS") {
res.status(200).end();
} else {
next();
}
});
app.get("/", function(req, res) {
res.send(" Real time POS web app running.");
});
app.use("/api/inventory", require("./api/inventory"));
app.use("/api", require("./api/transactions"));
// Websocket logic for Live Cart
io.on("connection", function(socket) {
socket.on("cart-transaction-complete", function() {
socket.broadcast.emit("update-live-cart-display", {});
});
  // on page load, show user current cart
socket.on("live-cart-page-loaded", function() {
socket.emit("update-live-cart-display", liveCart);
});
  // when client connected, make client update live cart
socket.emit("update-live-cart-display", liveCart);
  // when the cart data is updated by the POS
socket.on("update-live-cart", function(cartData) {
// keep track of it
liveCart = cartData;
    // broadcast updated live cart to all websocket clients
socket.broadcast.emit("update-live-cart-display", liveCart);
});
});
server.listen(port, () => console.log(`Listening on port ${port}`));

index.js Explained

This file is the entry point to our node express app. it is comprised of routes that will handle requests and responses to and from the browser.

Below are dependencies assigned to variables.

var express = require("express"),
http = require("http"),
port = 80,
app = require("express")(),
server = http.createServer(app),
bodyParser = require("body-parser"),
io = require("socket.io")(server),
liveCart

Below, The express variable app is used to allows data to be sent to the database using http request body.

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }))

Below are imported files that will represent inventory and transaction routes.

app.use("/api/inventory", require("./api/inventory"))

app.use("/api/transactions", require("./api/transactions"))

Cross-origin resource sharing (CORS) is a mechanism that allows restricted resources (e.g. fonts) on a web page to be requested from another domain outside the domain from which the first resource was served. — Wikipedia

Below, the node app is restricted to resources within using CORS and allows specified methods GET PUT POST DELETE and OPTIONS to be used.

app.all("/*", function(req, res, next) {
// CORS headers
res.header("Access-Control-Allow-Origin", "*"); // restrict it to the required domain
res.header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS");
// Set custom headers for CORS
res.header(
"Access-Control-Allow-Headers",
"Content-type,Accept,X-Access-Token,X-Key"
);
if (req.method == "OPTIONS") {
res.status(200).end();
} else {
next();
}
});

Below is the Node app default route

app.get("/", function(req, res) {
res.send(" Real time POS web app running.");
});

The Websocket logic for Live Cart

io.on("connection", function(socket) {
socket.on("cart-transaction-complete", function() {
socket.broadcast.emit("update-live-cart-display", {});
});

On page load, give user current cart

socket.on("live-cart-page-loaded", function() {
socket.emit("update-live-cart-display", liveCart);
});

On page load, make client update live cart

socket.emit("update-live-cart-display", liveCart)

When the cart data is updated by the POS and keeps track of it

socket.on("update-live-cart", function(cartData) {
liveCart = cartData;

Broadcasts updated live cart to all websocket clients

socket.broadcast.emit("update-live-cart-display", liveCart);
});

Let’s continue, create a directory inside server directory:

mkdir api

Create two files named inventory.js and transactions.js in your api folder

insert the following code to your inventory.js:

var app = require("express")();
var server = require("http").Server(app);
var bodyParser = require("body-parser");
var Datastore = require("nedb");
var async = require("async");
app.use(bodyParser.json());
module.exports = app;
// Creates  Database
var inventoryDB = new Datastore({
filename: "./server/databases/inventory.db",
autoload: true
});
// GET inventory
app.get("/", function(req, res) {
res.send("Inventory API");
});
// GET a product from inventory by _id
app.get("/product/:productId", function(req, res) {
if (!req.params.productId) {
res.status(500).send("ID field is required.");
} else {
inventoryDB.findOne({ _id: req.params.productId }, function(err, product) {
res.send(product);
});
}
});
// GET all inventory products
app.get("/products", function(req, res) {
inventoryDB.find({}, function(err, docs) {
console.log("sending inventory products");
res.send(docs);
});
});
// Create inventory product
app.post("/product", function(req, res) {
var newProduct = req.body;
  inventoryDB.insert(newProduct, function(err, product) {
if (err) res.status(500).send(err);
else res.send(product);
});
});
app.delete("/product/:productId", function(req, res) {
inventoryDB.remove({ _id: req.params.productId }, function(err, numRemoved) {
if (err) res.status(500).send(err);
else res.sendStatus(200);
});
});
// Updates inventory product
app.put("/product", function(req, res) {
var productId = req.body._id;
  inventoryDB.update({ _id: productId }, req.body, {}, function(
err,
numReplaced,
product
) {
if (err) res.status(500).send(err);
else res.sendStatus(200);
});
});
app.decrementInventory = function(products) {
async.eachSeries(products, function(transactionProduct, callback) {
inventoryDB.findOne({ _id: transactionProduct._id }, function(
err,
product
) {
// catch manually added items (don't exist in inventory)
if (!product || !product.quantity_on_hand) {
callback();
} else {
var updatedQuantity =
parseInt(product.quantity_on_hand) -
parseInt(transactionProduct.quantity);
        inventoryDB.update(
{ _id: product._id },
{ $set: { quantity_on_hand: updatedQuantity } },
{},
callback
);
}
});
});
};

inventory.js Explained
The necessary dependencies are assigned to variables app, server, bodyParser and Datastore. The app.use(bodyParser.json()) will allow the body of a http request to sent to the database.

An inventory variable inventoryDB is assigned with an instance of
nedb variable Datastore we created earlier. The DataStore
instance has two options filename which specifies the path of the database and autoload, which autumatically loads the database if set to true.

The app.get("/, function(req, res) function is the default path for the inventory database.

The app.get("/product/:/productId function enables the app to get a product from the inventory database using it’s ID.

The app.get("/products", function(req, res) function gets all products from the inventory database.

The app.post("/product", function(req, res) function is used to save an inventory product to the database.

The app.delete("/product/:productId", function(req, res) is used to delete product using product ID.

The app.put("/product", function(req, res) updates a product using it product ID.

Let’s continue, insert the following code to your transaction.js file:

var app      = require('express')()
var server = require('http').Server(app)
var bodyParser = require('body-parser')
var Datastore = require('nedb')
var Inventory = require('./inventory')
app.use(bodyParser.json())
module.exports = app
// Create Database
var Transactions = new Datastore({
filename: './server/databases/transactions.db',
autoload: true
})
app.get('/', function (req, res) {
res.send('Transactions API')
})
// GET all transactions
app.get('/all', function (req, res) {
        Transactions.find({}, function (err, docs) {
res.send(docs)
})
})
// GET all transactions
app.get('/limit', function (req, res) {
        var limit = parseInt(req.query.limit, 10)
if (!limit) limit = 5
        Transactions.find({}).limit(limit).sort({ date: -1 }).exec(function (err, docs) {
res.send(docs)
})
})
// GET total sales for the current day
app.get('/day-total', function (req, res) {
     // if date is provided
if (req.query.date) {
startDate = new Date(req.query.date)
startDate.setHours(0,0,0,0)
          endDate = new Date(req.query.date)
endDate.setHours(23,59,59,999)
}
else {
               // beginning of current day
var startDate = new Date()
startDate.setHours(0,0,0,0)
          // end of current day
var endDate = new Date()
endDate.setHours(23,59,59,999)
}
    Transactions.find({ date: { $gte: startDate.toJSON(), $lte: endDate.toJSON() } }, function (err, docs) {

var result = {
date: startDate
}

            if (docs) {
                  var total = docs.reduce(function (p, c) {
return p + c.total
}, 0.00)
                     result.total = parseFloat(parseFloat(total).toFixed(2))
                      res.send(result)
}
else {
result.total = 0
res.send(result)
}
})
})
// GET transactions for a particular date
app.get('/by-date', function (req, res) {

var startDate = new Date(2018, 2, 21)
startDate.setHours(0,0,0,0)

  var endDate = new Date(2015, 2, 21)
endDate.setHours(23,59,59,999)
       Transactions.find({ date: { $gte: startDate.toJSON(), $lte: endDate.toJSON() } }, function (err, docs) {
if (docs)
res.send(docs)
})
})
// Add new transaction
app.post('/new', function (req, res) {
  var newTransaction = req.body

Transactions.insert(newTransaction, function (err, transaction) {
if (err)
res.status(500).send(err)
else {
res.sendStatus(200)
Inventory.decrementInventory(transaction.products)
}
})
})

// GET a single transaction
app.get('/:transactionId', function (req, res) {
   Transactions.find({ _id: req.params.transactionId }, function (err, doc) {
if (doc)
res.send(doc[0])
})
})

transaction.js Explained
The necessary dependencies are assigned to variables as was done previously.

A Transaction’s variable is created with filename and autoload using the nedbvariable Datastore as done earlier.

The app.get("/, function(req, res) function is the default path for the transactions database.

The app.get('/all', function (req, res) function retrieves all transactions from the transactions database.

The app.get('/limit', function (req, res) function retrieves transactions with specified limit.

The app.get('/day-total', function (req, res) function is gets total sales for the current day.

The app.get('/by-date', function (req, res) function is used to get transactions using a particular date

The app.post('/new', function (req, res)) function is used to add new a transaction

The app.get('/:transactionId', function (req, res) function is used to retrieve a single transaction.

To Start Node app from root directory using CLI , type command:

nodemon index.js

end in backend section

Frontend part

We are going to accomplish the following:

1.Creating a Template React app.
2.Creating Routes and Views with Code Description.

See here for source code
Frameworks we will be using:

axios is a Promise based HTTP client for the browser and node.js.

Bootstrap is a free open source library that contains HTML and CSS design templates for designing websites and web applications.

React-Bootstrap is a Bootstrap 3 components built with React.

moment is a lightweight JavaScript date library for parsing, validating, manipulating, and formatting dates.

React is a JavaScript library for building user interfaces.

Creating a Template React App

Make sure you have Node and NPM installed.

Check Node and Npm Version via Command Line Interface (CLI)

node -v

npm -v

Access the real-time-pos-folder we used in part 1 Using CLI to create react app globally using npm:

For npm version 5.1 or earlier
npm install -g create-react-app

To create your app, run a single command
npm install create-react-app react-pos

For npm version 5.2+ and higher
npx install -g create-react-app

To create our appointment scheduler app, run a single command
npx install create-react-app react-pos

The Directory of your app will look something like this:

react-pos
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│ └── favicon.ico
│ └── index.html
│ └── manifest.json
└── src
└── App.css
└── App.js
└── App.test.js
└── index.css
└── index.js
└── logo.svg
└── registerServiceWorker.js

To start the project in development mode via CLI
npm start

Access your app directory using:
cd react-pos

Install the following dependencies:
npm install bootstrap

npm install react-bootstrap

npm install axios

npm install moment

Creating Routes and Views

We are going to start by creating our routes

Start by editing your App.js in your root directory with following code:

import React from "react";
import Header from "./js/components/Header";
import Main from "./js/components/Main";
const App = () => (



);
export default App;

Also update your index.js in your root directory:

import React from "react";
import { render } from "react-dom";
import { BrowserRouter } from "react-router-dom";
import registerServiceWorker from "./registerServiceWorker";
import "./index.css";
import "bootstrap/dist/css/bootstrap.css";
import { makeRoutes } from "./routes";
import App from "./App";
render(


,
document.getElementById("root")
);
registerServiceWorker();

You maybe wondering about the Main and Header Components, but we will create them shortly:

Create the following path in your “src” folder directory of your react-pos app:

js/components

Create Main.js in the js/components folder with the following code:

import React from "react";
import { Switch, Route } from "react-router-dom";
import Inventory from "./Inventory";
import Pos from "./Pos";
import Transactions from "./Transactions";
import LiveCart from "./LiveCart";
const Main = () => (








);
export default Main;

Notice that our Main.js component is not a class; rather it is a functional component,. Arrow function to be precise. we are creating our routes using functions.

Lets create our Header.js component for navigation of our app

import React from "react";
import { Link } from "react-router-dom";
// The Header creates links that can be used to navigate
// between routes.
const Header = () => (
    


  • Inventory


  • POS


  • Transactions


  • LiveCart



);
export default Header;

you will notice as we continue that the Header component is included in all parent components.

Now lets create our views, Let’s start with the Inventory.js component in the src/js/component/ folder.

import React, { Component } from "react";
import "./App.css";
import Header from "./Header";
import Product from "./Product";
import axios from "axios";
const HOST = "http://localhost:80";
class Inventory extends Component {
constructor(props) {
super(props);
    this.state = { products: [] };
}
componentWillMount() {
var url = HOST + `/api/inventory/products`;
axios.get(url).then(response => {
this.setState({ products: response.data });
});
}
render() {
var { products } = this.state;
    var renderProducts = () => {
if (products.length === 0) {
return

{products}

;
}
return products.map(product => );
};
    return (

        

href="#/inventory/create-product"
class="btn btn-success pull-right"
>
Add New Item




          







{renderProducts()}
NamePriceQuantity on Hand



);
}
}
export default Inventory;

Notice that We are using a class for the inventory component above. componentWillMount is a Lifecycle method which is used to modify the component state, in this particular situation we are retrieving products from the inventory database by through our Node.js Express App We created in part 1. the response is assigned to the product array using setState . All this is done before the page is fully loaded.

The render function will display our UI elements in the DOM (Document Object Model). The renderFunction checks the product array and display the result in the DOM.

Let’s move on to the POS.js Component. The Pos component will allows the user to add items to cart with prices. the cart will be updated in real time.

Create a Pos.js file in src/js/component/ folder:

import React, { Component } from "react";
import "./App.css";
import Header from "./Header";
import io from "socket.io-client";
import axios from "axios";
import moment from "moment";
import { Modal, Button } from "react-bootstrap";
import LivePos from "./LivePos";
const HOST = "http://localhost:80";
let socket = io.connect(HOST);
class Pos extends Component {
constructor(props) {
super(props);
this.state = {
items: [],
quantity: 1,
id: 0,
open: true,
close: false,
addItemModal: false,
checkOutModal: false,
amountDueModal: false,
totalPayment: 0,
total: 0,
changeDue: 0,
name: "",
price: 0
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleName = this.handleName.bind(this);
this.handlePrice = this.handlePrice.bind(this);
this.handlePayment = this.handlePayment.bind(this);
this.handleQuantityChange = this.handleQuantityChange.bind(this);
this.handleCheckOut = this.handleCheckOut.bind(this);
}
componentDidUpdate() {
if (this.state.items.length !== 0) {
socket.emit("update-live-cart", this.state.items);
}
}
handleSubmit = e => {
e.preventDefault();
this.setState({ addItemModal: false });
    const currentItem = {
id: this.state.id++,
name: this.state.name,
price: this.state.price,
quantity: this.state.quantity
};
var items = this.state.items;
items.push(currentItem);
this.setState({ items: items });
};
handleName = e => {
this.setState({ name: e.target.value });
};
handlePrice = e => {
this.setState({ price: e.target.value });
};
handlePayment = () => {
this.setState({ checkOutModal: false });
var amountDiff =
parseInt(this.state.total, 10) - parseInt(this.state.totalPayment, 10);
if (this.state.total <= this.state.totalPayment) {
this.setState({ changeDue: amountDiff });
this.setState({ receiptModal: true });
this.handleSaveToDB();
this.setState({ items: [] });
this.setState({ total: 0 });
} else {
this.setState({ changeDue: amountDiff });
this.setState({ amountDueModal: true });
}
};
handleQuantityChange = (id, quantity) => {
var items = this.state.items;
for (var i = 0; i < items.length; i++) {
if (items[i].id === id) {
items[i].quantity = quantity;
this.setState({ items: items });
}
}
};
handleCheckOut = () => {
this.setState({ checkOutModal: true });
var items = this.state.items;
var totalCost = 0;
for (var i = 0; i < items.length; i++) {
var price = items[i].price * items[i].quantity;
totalCost = parseInt(totalCost, 10) + parseInt(price, 10);
}
this.setState({ total: totalCost });
};
handleSaveToDB = () => {
const transaction = {
date: moment().format("DD-MMM-YYYY HH:mm:ss"),
total: this.state.total,
items: this.state.items
};
axios.post(HOST + "/api/new", transaction).catch(err => {
console.log(err);
});
};
render() {
var { quantity, modal, items } = this.state;
    var renderAmountDue = () => {
return (


Amount



Amount Due:
{this.state.changeDue}


Customer payment incomplete; Correct and Try again







);
};
var renderReceipt = () => {
return (


Receipt



Total:
{this.state.totalPayment}



Change Due:
{this.state.changeDue}







);
};
    var renderLivePos = () => {
if (items.length === 0) {
return

No products added

;
} else {
return items.map(
item => (

),
this
);
}
};
    return (




Total



${this.state.total}



class="btn btn-success lead"
id="checkoutButton"
onClick={this.handleCheckOut}
>





C

h

e

c

k

o

u

t




Checkout




Total:
{this.state.total}

                      class="form-horizontal"
name="checkoutForm"
onSubmit={this.handlePayment}
>


$

type="number"
id="checkoutPaymentAmount"
class="form-control input-lg"
name="payment"
onChange={event =>
this.setState({
totalPayment: event.target.value
})
}
min="0"
/>

                        

Enter payment amount.



class="btn btn-primary btn-lg lead"
onClick={this.handlePayment}
>
Print Receipt





onClick={() => this.setState({ checkOutModal: false })}
>
Close