Inventory Management System With Pure Javascript (PWA)

Welcome to a tutorial on how to create an inventory management system with pure Javascript. If you are wondering if it is possible to build an entire inventory system using HTML, CSS, and client-side Javascript only – Yes, it is very possible. But be warned, this is more of an “experiment” and not a “complete system”.

This is here to showcase how modern Javascript is capable of handling heavy lifting without a server, otherwise also known as a “serverless setup” and “progressive web app” (PWA). With that, read on for the example!

ⓘ I have included a zip file with all the source code at the start of this tutorial, so you don’t have to copy-paste everything… Or if you just want to dive straight in.

 

 

TABLE OF CONTENTS

Download & Notes JS Inventory Useful Bits & Links
The End

 

DOWNLOAD & NOTES

Firstly, here is the download link to the example code as promised.

 

QUICK NOTES

  • Download and unzip into your HTTP folder.
  • Access js-inventory.html in the browser.
  • Captain Obvious to the rescue – Use http://, not file://.
  • Not newbie-friendly, but a good study for advanced Javascript and PWA nonetheless.
If you spot a bug, feel free to comment below. I try to answer short questions too, but it is one person versus the entire world… If you need answers urgently, please check out my list of websites to get help with programming.

 

EXAMPLE CODE DOWNLOAD

Click here to download all the example source code, I have released it under the MIT license, so feel free to build on top of it or use it in your own project.

 

 

JAVASCRIPT INVENTORY SYSTEM

All right, let us now get into some details of the experimental Javascript inventory system.

 

PART 1) THE HTML

js-inventory.html
<!-- (A) PAGE A : ITEMS LIST -->
<div id="demoA" class="zebra"></div>
 
<!-- (B) PAGE B : MOVEMENT -->
<div id="demoB" class="ninja">
  <!-- (B1) MOVEMENT FORM -->
  <h3>ADD MOVEMENT</h3>
  <form id="demoBA" onsubmit="inv.saveMvt(); return false;">
    <label>SKU</label>
    <input type="text" readonly id="mSKU"/>
    <label>Direction</label>
    <select id="mDirect">
      <option value="I">In</option>
      <option value="O">Out</option>
      <option value="T">Stock Take</option>
    </select>
    <label>Quantity</label>
    <input type="number" required id="mQty"/>
    <input type="submit" value="Save"/>
  </form>
 
  <!-- (B2) MOVEMENT HISTORY -->
  <h3>MOVEMENT HISTORY</h3>
  <div id="demoBB" class="zebra"></div>
  <input type="button" value="Back" onclick="inv.pgTog('A')"/>
</div>

For a start, let us deal with the HTML page itself, this is quite essentially just “2 screens in 1 page”.

  1. <div id="demoA"> default first page that lists all the items; We will use Javascript to generate this list later.
  2. <div id="demoB"> When the user clicks on the “Movement” button, this page will display an “item movement” form, as well as the movement history.

 

 

PART 2) INITIATING THE WEB APP

js-inventory.js
// (A) INDEXED DB
const IDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
 
var inv = {
  // (B) INITIALIZE APP
  idb : null,
  init : () => {
    // (B1) CHECK - INDEXED DATABASE SUPPORT
    if (!IDB) {
      alert("INDEXED DB IS NOT SUPPORTED ON THIS BROWSER!");
      return false;
    }
 
    // (B2) CHECK - SERVICE WORKER SUPPORT
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.register("worker.js");
    } else {
      alert("SERVICE WORKER IS NOT SUPPORTED ON THIS BROWSER!");
      return false;
    }

    // (B3) OPEN INVENTORY DATABASE
    let install = false;
    inv.idb = IDB.open("JSINV", 1);

    // (B4) CREATE INVENTORY DATABASE
    inv.idb.onupgradeneeded = (evt) => {
      // (B4-1) INVENTORY DATABASE
      inv.idb = evt.target.result;
 
      // (B4-2) ITEMS STORE (TABLE)
      let store = inv.idb.createObjectStore("Items", {keyPath: "sku"});
      store.createIndex("name", "name");
 
      // (B4-3) MOVEMENT STORE (TABLE)
      store = inv.idb.createObjectStore("Movement", {keyPath: ["sku", "date"]}),
      store.createIndex("direction", "direction");
 
      // (B4-4) INSTALL DUMMY DATA
      // KIND OF DUMB, BUT CAN ONLY INSERT DATA AFTER DB UPGRADE
      install = true;
    };
 
    // (B5) ON IDB OPEN
    inv.idb.onsuccess = (evt) => {
      // (B5-1) INVENTORY DATABASE
      inv.idb = evt.target.result;
 
      // (B5-2) INSERT DUMMY DATA
      if (install) {
        inv.iPut("Items", { sku: "ABC123", name: "Foo Bar", qty: 123 });
        inv.iPut("Items", { sku: "BCD234", name: "Goo Bar", qty: 321 });
        inv.iPut("Items", { sku: "CDE345", name: "Hoo Bar", qty: 231 });
        inv.iPut("Items", { sku: "DEF456", name: "Joo Bar", qty: 213 });
        inv.iPut("Items", { sku: "EFG567", name: "Koo Bar", qty: 312 });
        inv.iPut("Movement", { sku: "ABC123", date: Date.now()-30000, direction: "T", qty: 123 });
        inv.iPut("Movement", { sku: "ABC123", date: Date.now()-20000, direction: "O", qty: 23 });
        inv.iPut("Movement", { sku: "ABC123", date: Date.now()-10000, direction: "O", qty: 32 });
        inv.iPut("Movement", { sku: "ABC123", date: Date.now(), direction: "I", qty: 55 });
      }
 
      // (B5-3) HTML INTERFACE
      inv.pgA = document.getElementById("demoA");
      inv.pgB = document.getElementById("demoB");
      inv.pgItems(true);
    };
  }
};
window.addEventListener("DOMContentLoaded", inv.init);
  • (A) Indexed database plays a huge part in this mini-project to store all the data. At the time of writing, different browsers still use their own prefix, and thus the need for const IDB.
  • (B) inv.init() is the first thing that runs on page load. Looks massive, but it is straightforward once you read it section-by-section.
    • Pre-launch checks – The browser must support indexed databases and service workers.
    • Register a service worker.
    • Create the database and “insert” dummy item entries.
    • Draw the HTML interface when ready.

 

PART 3) THE DATABASE “STRUCTURE”

Now, a quick summary of the database structure before we move on. There are 2 stores (tables if you are used to SQL) in this project.

  • Items The available items.
    • sku  The SKU of the item, primary key.
    • name Name of the item.
    • qty The current quantity of the item.
  • Movement The movement history of items.
    • sku SKU of the item, composite primary key.
    • date Timestamp of the movement, composite primary key.
    • direction “I”n, “O”ut, “S”tock Take.
    • qty Quantity moved.

 

 

PART 4) INDEXED DATABASE SUPPORT FUNCTIONS

js-inventory.js
// (C) IDB SUPPORT FUNCTIONS
// (C1) PUT
iPut : (store, data) => {
  return new Promise((resolve, reject) => {
    let req = inv.idb.transaction(store, "readwrite").objectStore(store).put(data);
    req.onsuccess = (evt) => { resolve(true); };
    req.onerror = (evt) => { reject(event.target.error); };
  });
},
 
// (C2) GET
iGet : (store, key) => {
  return new Promise((resolve, reject) => {
    let res = inv.idb.transaction(store, "readonly").objectStore(store).get(key);
    res.onsuccess = (evt) => resolve(evt.target.result);
    res.onerror = (evt) => reject(event.target.error);
  });
},
 
// (C3) GET ALL
iGetAll : (store, range) => {
  return new Promise((resolve, reject) => {
    let res = inv.idb.transaction(store, "readonly").objectStore(store).getAll(range);
    res.onsuccess = (evt) => resolve(evt.target.result);
    res.onerror = (evt) => reject(event.target.error);
  });
},

Next, we have a whole bunch of “SELECT INSERT UPDATE” indexed database support functions… Let’s just say that it is not the friendliest to work with indexed databases yet.

  • iPut() The equivalent of doing INSERT or UPDATE in SQL.
  • iGet() Does a SELECT, returns a single row.
  • iGetAll() Does a SELECT, returns multiple rows.

 

PART 5) INVENTORY SUPPORT FUNCTIONS

js-inventory.js
// (D) INVENTORY SYSTEM FUNCTIONS
// (D1) GET ALL ITEMS
getItems : async () => {
  return await inv.iGetAll("Items");
},
 
// (D2) GET ITEM
getItem : async (sku) => {
  return await inv.iGet("Items", sku);
},
 
// (D3) GET ITEM MOVEMENT HISTORY
getMove : async (sku) => {
  return await inv.iGetAll("Movement", IDBKeyRange.bound([sku, 0], [sku, Date.now()]));
},
 
// (D4) SAVE MOVEMENT
putMove : async (sku, direction, qty) => {
  // (D4-1) GET ITEM
  let item = await inv.getItem(sku), newqty;
  if (item == undefined) { return false; }
 
  // (D4-2) NEW QUANTITY
  item["qty"] = parseInt(item["qty"]);
  qty = parseInt(qty);
  if (direction == "T") { newqty = qty; }
  else if (direction == "I") { newqty = item["qty"] + qty; }
  else { newqty = item["qty"] - qty; }
  if (newqty<0) { newqty = 0; }
 
  // (D4-3) UPDATE QUANTITY
  await inv.iPut("Items", {
    sku: item["sku"],
    name: item["name"],
    qty: newqty
  });
 
  // (D4-4) ADD MOVEMENT
  await inv.iPut("Movement", {
    sku: item["sku"],
    date: Date.now(),
    direction: direction,
    qty: qty
  });
},

Following up with the indexed database support functions, you can call this section the “inventory library”.

  • getItems() Get all items in the database.
  • getItem() Get the item with the specified SKU.
  • getMove() Get the movement history of the specified item.
  • putMove() Add a new movement to the item.

 

 

PART 6) HTML INTERFACE FUNCTIONS

js-inventory.js
// (E) HTML INTERFACE
pgA : null, pgB : null,
 
// (E1) TOGGLE PAGE
pgTog : (pg) => {
  inv.pgA.classList.add("ninja");
  inv.pgB.classList.add("ninja");
  document.getElementById("demo" + pg).classList.remove("ninja");
},
 
// (E2) DRAW ALL ITEMS
pgItems : async (toggle) => {
  // (E2-1) GET ALL ITEMS
  let items = await inv.getItems();
 
  // (E2-2) DRAW HTML LIST
  inv.pgA.innerHTML = "";
  for (let i of items) {
    let row = document.createElement("div");
    row.className = "row";
    row.innerHTML =
    `<div class="left">
      [${i["sku"]}] ${i["name"]}<br>
      Quantity ${i["qty"]}
    </div>
    <div class="right">
      <input type="button" value="Movement" onclick="inv.pgMove('${i["sku"]}')"/>
    </div>`;
    inv.pgA.appendChild(row);
  }
  if (toggle) { inv.pgTog("A"); }
},
 
// (E3) SHOW MOVEMENT
pgMove : async (sku) => {
  // (E3-1) TOGGLE SCREEN
  inv.pgTog("B");
 
  // (E3-2) "PRESET" MOVEMENT FORM
  document.getElementById("mSKU").value = sku;
  document.getElementById("mDirect").value = "I";
  document.getElementById("mQty").value = 1;
 
  // (E3-3) DRAW MOVEMENT HISTORY
  let mvt = await inv.getMove(sku),
      list = document.getElementById("demoBB"),
      d = { "I" : "In", "O" : "Out", "T" : "Stock Take" };
 
  if (mvt.length == 0) {
    list.innerHTML = "<div class='row'>No history.</div>";
  } else { for (let m of mvt) {
    let row = document.createElement("div"),
        date = new Date(m["date"]).toString();
    row.className = "row";
    row.innerHTML =
    `<div class="left">
       <strong>${d[m["direction"]]}</strong><br>${date}
     </div>
     <div class="right">${m["qty"]}</div>`;
    list.appendChild(row);
  }}
},
 
// (F) SAVE MOVEMENT
saveMvt : async () => {
  // (F1) ADD MOVEMENT ENTRY
  let sku = document.getElementById("mSKU").value;
  await inv.putMove(
    sku,
    document.getElementById("mDirect").value,
    document.getElementById("mQty").value
  );
 
  // (F2) UPDATE MOVEMENT HISTORY & ITEMS LIST
  inv.pgMove(sku);
  inv.pgItems();
  return false;
}

The last part of the Javascript deals with the HTML interface.

  • pgTog() Toggle between the “items list” and “item movement” pages.
  • pgItems() Draws all the items.
  • pgMove() Show the item movement history.
  • saveMvt() Save the item movement.

 

PART 7) PROGRESSIVE WEB APP

THE HTML

js-inventory.html
<!-- META -->
<title>JS Inventory Management</title>
<meta charset="utf-8">
<meta name="description" content="Experimental JS Inventory System">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.5">
 
<!-- ICONS -->
<link rel="icon" href="images/favicon.png" type="image/png">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="white">
<link rel="apple-touch-icon" href="images/icon-512.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="JS POS">
<meta name="msapplication-TileImage" content="images/icon-512.png">
<meta name="msapplication-TileColor" content="#ffffff">
 
<!-- MANIFEST -->
<link rel="manifest" href="manifest.json">
 
<!-- INVENTORY CSS + JS -->
<link rel="stylesheet" href="js-inventory.css">
<script src="js-inventory.js"></script>

At this stage, we already have a working web app. But let’s bring it one step forward, and make it into a PWA. Some people think that it is very complicated, but it’s really not that bad. The essential pieces are:

  • Define a manifest file <link rel="manifest" href="manifest.json">
  • Register a service worker (in the Javascript section B2), and cache the critical files.

That’s about it.

 

 

MANIFEST FILE

manifest.json
{
  "short_name": "JS Inventory",
  "name": "JS Inventory",
  "icons": [{
    "src": "images/favicon.png",
    "sizes": "64x64",
    "type": "image/png"
  }, {
    "src": "images/icon-512.png",
    "sizes": "512x512",
    "type": "image/png"
  }],
  "start_url": "js-inventory.html",
  "scope": "/",
  "background_color": "white",
  "theme_color": "white",
  "display": "standalone"
}

Nothing much here. The manifest file simply specifies the web app name, icons, colors, and all those misc stuff.

 

SERVICE WORKER

worker.js
// (A) FILES TO CACHE
const cName = "demo-pwa",
cFiles = [
  // (A1) INVENTORY "SYSTEM"
  "js-inventory.html",
  "js-inventory.css",
  "js-inventory.js",
 
  // (A2) IMAGES
  "images/favicon.png",
  "images/icon-512.png"
];
 
// (B) CREATE/INSTALL CACHE
self.addEventListener("install", (evt) => {
  evt.waitUntil(
    caches.open(cName)
    .then((cache) => { return cache.addAll(cFiles); })
    .catch((err) => { console.error(err) })
  );
});
 
// (C) LOAD FROM CACHE FIRST, FALLBACK TO NETWORK IF NOT FOUND
self.addEventListener("fetch", (evt) => {
  evt.respondWith(
    caches.match(evt.request)
    .then((res) => { return res || fetch(evt.request); })
  );
});

Long story short – Save the entire app into the browser cache. That is, it works offline without the server.

 

USEFUL BITS & LINKS

That’s all for the tutorial, and here is a small section on some extras and links that may be useful to you.

 

COMPATIBILITY CHECKS

This example requires a “Grade A” browser.

 

IT WORKS… BUT DOES IT MAKES SENSE?

Yep, we can build up this experiment into a “full system” with some more effort:

  • User management
  • User login
  • Items management
  • Generate reports

But here comes the million-dollar question, is it worth the effort to build this system?

  • While this is essentially “serverless”, the initial setup still requires http:// https:// to install properly.
  • How about future app updates? Still requires a server.
  • How about data backup? Still requires a server.
  • Trying to sync data across several decentralized devices is another headache.
  • Indexed database is clunky and basic compared to SQL.

So my personal conclusion is simple – This inventory PWA works for a single device. But otherwise, a server-client setup is still preferable.

 

LINKS & REFERENCES

 

THE END

Thank you for reading, and we have come to the end. I hope that it has helped you to better understand, and if you want to share anything with this guide, please feel free to comment below. Good luck and happy coding!

Leave a Comment

Your email address will not be published.