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
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://
, notfile://
. - Not newbie-friendly, but a good study for advanced Javascript and PWA nonetheless.
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
<!-- (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”.
<div id="demoA">
default first page that lists all the items; We will use Javascript to generate this list later.<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
// (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
// (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 doingINSERT
orUPDATE
in SQL.iGet()
Does aSELECT
, returns a single row.iGetAll()
Does aSELECT
, returns multiple rows.
PART 5) INVENTORY SUPPORT FUNCTIONS
// (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
// (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
<!-- 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
{
"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
// (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
- Arrow Functions – CanIUse
- Indexed Database – CanIUse
- Service Workers – CanIUse
- Add To Home Screen – CanIUse
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
- Storage Boxx PHP Inventory Management System – Code Boxx
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!