Skip to content

Instantly share code, notes, and snippets.

@code-boxx
Last active March 6, 2024 15:31
Show Gist options
  • Save code-boxx/9c8e5836d0b7ecd92956d931e8a0af2c to your computer and use it in GitHub Desktop.
Save code-boxx/9c8e5836d0b7ecd92956d931e8a0af2c to your computer and use it in GitHub Desktop.
PHP MYSQL POS

SIMPLE PHP MYSQL POS SYSTEM

https://code-boxx.com/pos-system-php-mysql/

IMAGES

favicon icon-512 img-a img-b img-c img-d img-e img-f

NOTES

  1. Run unpack.bat (Windows) unpack.sh (Linux/Mac), this will create an assets folder and download all the above images.
  2. Create a database and import 1-database.sql.
  3. Change the database settings in 2-lib-pos.php to your own.
  4. Launch 4a-pos.html in the web browser.

LICENSE

Copyright by Code Boxx

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

-- (A) ITEMS
CREATE TABLE `items` (
`item_id` bigint(20) NOT NULL,
`item_name` varchar(255) NOT NULL,
`item_price` decimal(12,2) NOT NULL,
`item_image` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `items`
ADD PRIMARY KEY (`item_id`),
ADD KEY `item_name` (`item_name`);
ALTER TABLE `items`
MODIFY `item_id` bigint(20) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7;
INSERT INTO `items` (`item_id`, `item_name`, `item_price`, `item_image`) VALUES
(1, 'Penne Fried Rice', '1.10', 'img-a.png'),
(2, 'Pineapple Pizza', '2.20', 'img-b.png'),
(3, 'Ice Cream Rice', '3.30', 'img-c.png'),
(4, 'Sushi Pancake', '4.40', 'img-d.png'),
(5, 'Vegan Rice', '5.50', 'img-e.png'),
(6, 'Candy Soup', '6.60', 'img-f.png');
-- (B) ORDERS
CREATE TABLE `orders` (
`order_id` bigint(20) NOT NULL,
`oder_date` datetime NOT NULL DEFAULT current_timestamp(),
`order_total` decimal(12,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `orders`
ADD PRIMARY KEY (`order_id`),
ADD KEY `oder_date` (`oder_date`);
ALTER TABLE `orders`
MODIFY `order_id` bigint(20) NOT NULL AUTO_INCREMENT;
-- (C) ORDER ITEMS
CREATE TABLE `order_items` (
`order_item_id` bigint(20) NOT NULL,
`order_id` bigint(20) NOT NULL,
`item_name` varchar(255) NOT NULL,
`item_price` decimal(12,0) NOT NULL,
`item_qty` bigint(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `order_items`
ADD PRIMARY KEY (`order_item_id`),
ADD KEY `order_id` (`order_id`),
ADD KEY `item_name` (`item_name`);
ALTER TABLE `order_items`
MODIFY `order_item_id` bigint(20) NOT NULL AUTO_INCREMENT;
<?php
class POS {
// (A) CONSTRUCTOR - CONNECT TO DATABASE
private $pdo = null;
private $stmt = null;
public $error = "";
function __construct () {
$this->pdo = new PDO(
"mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=".DB_CHARSET,
DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
}
// (B) DESTRUCTOR - CLOSE DATABASE CONNECTION
function __destruct () {
if ($this->stmt!==null) { $this->stmt = null; }
if ($this->pdo!==null) { $this->pdo = null; }
}
// (C) HELPER FUNCTION - EXECUTE SQL QUERY
function query ($sql, $data=null) : void {
$this->stmt = $this->pdo->prepare($sql);
$this->stmt->execute($data);
}
// (D) SAVE ITEM
function saveItem ($name, $price, $img=null, $id=null) {
// (D1) NEW ITEM
if ($id==null) {
$sql = "INSERT INTO `items` (`item_name`, `item_price`, `item_image`) VALUES (?,?,?)";
$data = [$name, $price, $img];
}
// (D2) UPDATE ITEM
else {
$sql = "UPDATE `items` SET `item_name`=?, `item_price`=?, `item_image`=? WHERE `item_id`=?";
$data = [$name, $price, $img, $id];
}
// (D3) RUN SQL + UPDATE TIMESTAMP
// YOU MIGHT WANT TO KEEP THIS IN THE DATABASE INSTEAD...
$this->query($sql, $data);
file_put_contents(
__DIR__ . DIRECTORY_SEPARATOR . "updated.php",
"<?php define('POS_UPDATE', ".strtotime("now").");"
);
return true;
}
// (E) CHECKOUT ORDER
function checkout ($items, $total, $timestamp) {
// (E1) CHECK TIMESTAMP
if ($timestamp < POS_UPDATE) {
$this->error = POS_UPDATE;
return false;
}
// (E2) ORDERS ENTRY
$this->query(
"INSERT INTO `orders` (`order_total`) VALUES (?)", [$total]
);
// (E3) ORDER ITEMS
$id = $this->pdo->lastInsertId();
$sql = "INSERT INTO `order_items` (`order_id`, `item_name`, `item_price`, `item_qty`) VALUES ";
$data = [];
$items = json_decode($items, true);
foreach ($items as $i) {
$sql .= "(?,?,?,?),";
$data[] = $id;
$data[] = $i["n"];
$data[] = $i["p"];
$data[] = $i["q"];
}
$sql = substr($sql, 0, -1) . ";";
$this->query($sql, $data);
// (E4) DONE
return true;
}
// (F) GET ALL ITEMS
function getAll () {
$this->query("SELECT * FROM `items`");
return $this->stmt->fetchAll();
}
}
// (G) DATABASE SETTINGS - CHANGE TO YOUR OWN!
define("DB_HOST", "localhost");
define("DB_NAME", "test");
define("DB_CHARSET", "utf8mb4");
define("DB_USER", "root");
define("DB_PASSWORD", "");
// (H) LAST UPDATED
require "updated.php";
// (I) NEW POS OBJECT
$_POS = new POS();
<?php
require "2-lib-pos.php";
if (isset($_POST["req"])) { switch ($_POST["req"]) {
// (A) CHECK LAST UPDATE
case "check":
echo POS_UPDATE;
break;
// (B) GET ALL ITEMS
case "getAll":
echo json_encode($_POS->getAll());
break;
// (C) CHECKOUT ORDER
case "checkout":
echo $_POS->checkout($_POST["items"], $_POST["total"], $_POST["timestamp"])
? "OK" : $_POS->error ;
break;
}}
<!DOCTYPE html>
<html>
<head>
<!-- TITLE + CHARSET + DESCRIPTION + VIEWPORT + FAVICON -->
<title>POS Page</title>
<meta charset="utf-8">
<meta name="description" content="PHP POS">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.5">
<link rel="icon" href="assets/favicon.png" type="image/png">
<!-- ANDROID + CHROME + APPLE + WINDOWS APP -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="white">
<link rel="apple-touch-icon" href="assets/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="PHP POS">
<meta name="msapplication-TileImage" content="assets/icon-512.png">
<meta name="msapplication-TileColor" content="#ffffff">
<!-- WEB APP MANIFEST -->
<!-- https://web.dev/add-manifest/ -->
<link rel="manifest" href="5a-manifest.json">
<!-- SERVICE WORKER -->
<script>
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("5b-worker.js");
}
</script>
<!-- STYLESHEET + JAVASCRIPT -->
<script src="4b-pos.js"></script>
<script src="4c-cart.js"></script>
<link rel="stylesheet" href="4d-pos.css">
</head>
<body>
<!-- (A) ITEMS LIST -->
<div id="left"><div id="list"></div></div>
<!-- (B) CART ITEMS -->
<div id="right"><div id="cart"></div></div>
</body>
</html>
var pos = {
// (A) PROPERTIES
iName : "POS", // idb name
iDB : null, iTX : null, // idb object & transaction
updated : null, // last updated timestamp
items : null, // items list
hList : null, // html items list
hCart : null, // html cart items
// (B) HELPER FUNCTION - AJAX FETCH
fetch : (req, data, after) => {
// (B1) FORM DATA
let form = new FormData();
form.append("req", req);
if (data != null) { for (let [k,v] of Object.entries(data)) {
form.append(k, v);
}}
// (B2) GO!
fetch("3-ajax-pos.php", { method: "post", body: form })
.then(res => res.text())
.then(txt => after(txt))
.catch(err => console.error(err));
},
// (C) HELPER FUNCTION - UPDATE INDEXED DATABASE ITEMS
update : ts => pos.fetch("getAll", null, items => {
// (C1) CLEAR OLD ITEMS
pos.iTX().clear();
// (C2) UPDATE ITEMS
items = JSON.parse(items);
let count = 0;
for (let i of items) {
let req = pos.iTX().put(i);
req.onsuccess = () => {
count++;
if (count==items.length) {
localStorage.setItem("POSUpdate", ts);
pos.draw(cart.empty);
alert("Item database updated.");
}
};
}
}),
// (D) INITIALIZE
init : () => {
// (D1) IDB SUPPORT CHECK
window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
if (!window.indexedDB) {
alert("Your browser does not support indexed database.");
return false;
}
// (D2) OPEN "POS" DATABASE
let req = window.indexedDB.open(pos.iName, 1);
// (D3) ON DATABASE ERROR
req.onerror = evt => {
alert("Indexed DB init error - " + evt.message);
console.error(evt);
};
// (D4) UPGRADE NEEDED
req.onupgradeneeded = evt => {
// (D4-1) INIT UPGRADE
pos.iDB = evt.target.result;
pos.iDB.onerror = evt => {
alert("Indexed DB upgrade error - " + evt.message);
console.error(evt);
};
// (D4-2) VERSION 1
if (evt.oldVersion < 1) {
let store = pos.iDB.createObjectStore(pos.iName, { keyPath: "item_id" });
}
};
// (D5) OPEN DATABASE OK
req.onsuccess = evt => {
// (D5-1) REGISTER IDB OBJECTS
pos.iDB = evt.target.result;
pos.iTX = () => {
return pos.iDB
.transaction(pos.iName, "readwrite")
.objectStore(pos.iName);
};
// (D5-2) GET HTML ELEMENTS
pos.hList = document.getElementById("list");
pos.hCart = document.getElementById("cart");
// (D5-3) LAST UPDATED - ITEMS
pos.updated = localStorage.getItem("POSUpdate");
if (pos.updated== null) { pos.updated = 0; }
// (D5-4) CHECK SERVER FOR ITEM UPDATES
pos.fetch("check", null, ts => {
if (ts > pos.updated) { pos.update(ts); }
else { pos.draw(() => { cart.load(); cart.draw(); }); }
});
};
},
// (E) GET & DRAW ITEMS
draw : after => {
let req = pos.iTX().getAll();
req.onsuccess = () => {
pos.hList.innerHTML = "";
pos.items = {};
for (let i of req.result) {
pos.items[i["item_id"]] = i;
let item = document.createElement("div");
item.className = "item";
item.innerHTML = `<img class="iImg" src="assets/${i["item_image"]}">
<div class="iName">${i["item_name"]}</div>
<div class="iPrice">$${i["item_price"]}</div>`;
item.onclick = () => { cart.add(i["item_id"]); };
pos.hList.appendChild(item);
}
if (after) { after(); }
};
}
};
window.onload = pos.init;
var cart = {
// (A) PROPERTIES
items : {}, // current items in cart
total : 0, // total amount
// (B) SAVE CART ITEMS TO LOCALSTORAGE
save : () => localStorage.setItem("POSCart", JSON.stringify(cart.items)),
// (C) LOAD CART ITEMS FROM LOCALSTORAGE
load : () => {
cart.items = localStorage.getItem("POSCart");
if (cart.items==null) { cart.items = {}; }
else { cart.items = JSON.parse(cart.items); }
},
// (D) DRAW CART ITEMS
draw : () => {
// (D1) RESET TOTAL
cart.total = 0;
// (D2) CART EMPTY
if (Object.keys(cart.items).length === 0) {
pos.hCart.innerHTML = `<div class="cItem">Cart Empty</div>`;
}
// (D3) DRAW CART
else {
// (D3-1) RESET HTML
pos.hCart.innerHTML = "";
// (D3-2) CART ITEMS
let item;
for (let [id, qty] of Object.entries(cart.items)) {
let itotal = pos.items[id]["item_price"] * qty;
cart.total += itotal;
item = document.createElement("div");
item.className = "cRow";
item.innerHTML = `<div class="cDel" onclick="cart.change(${id}, 0)">X</div>
<div class="cItem">
<div class="cName">${pos.items[id]["item_name"]}</div>
<div class="cPrice">$${itotal.toFixed(2)}</div>
</div>
<input type="number" min="0" class="cQty" onchange="cart.change(${id}, this.value)" value="${qty}">`;
pos.hCart.appendChild(item);
}
// (D3-3) TOTAL
item = document.createElement("div");
item.className = "cRow";
item.innerHTML = `<div class="cTotal">Total</div><div class="cAmt">$${cart.total.toFixed(2)}</div>`;
pos.hCart.appendChild(item);
// (D3-4) RESET
item = document.createElement("input");
item.type = "button";
item.value = "Reset";
item.onclick = cart.empty;
pos.hCart.appendChild(item);
// (D3-5) CHECKOUT
item = document.createElement("input");
item.type = "button";
item.value = "Checkout";
item.onclick = cart.checkout;
pos.hCart.appendChild(item);
}
},
// (E) ADD ITEM TO CART
add : id => {
if (cart.items[id]==undefined) { cart.items[id] = 1; }
else { cart.items[id]++; }
cart.save();
cart.draw();
},
// (F) CHANGE QUANTITY + REMOVE ITEM FROM CART
change : (id, qty) => {
if (qty==0) { delete cart.items[id]; }
else if (qty!="") { cart.items[id] = parseInt(qty); }
cart.save();
cart.draw();
},
// (G) EMPTY CART
empty : () => {
cart.items = {};
cart.save();
cart.draw();
},
// (H) CHECKOUT
checkout : () => {
// (H1) RESHUFFLE ITEMS
let items = [];
for (let [id, qty] of Object.entries(cart.items)) {
items.push({
n : pos.items[id]["item_name"],
p : pos.items[id]["item_price"],
q : qty
});
}
// (H2) SEND TO SERVER
pos.fetch("checkout", {
items : JSON.stringify(items),
total : cart.total.toFixed(2),
timestamp : pos.updated
}, res => {
// (H3) "LOCAL ITEMS OUTDATED"
if (isFinite(res)) { pos.update(parseInt(res)); }
// (H4) OK
else if (res=="OK") {
cart.empty();
alert("OK");
}
// (H5) NOT OK
else { alert(res); }
});
}
};
/* (A) ENTIRE PAGE */
* {
font-family: Arial, Helvetica, sans-serif;
box-sizing: border-box;
}
body {
display: flex;
max-width: 1000px;
min-height: 100vh;
margin: 0 auto;
padding: 0;
background: #222;
}
#left, #right { padding: 10px; }
#left { flex-grow: 1; }
#right {
width: 240px;
flex-shrink: 0;
}
/* (B) ITEMS LIST */
#list {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-gap: 10px;
}
@media only screen and (max-width: 800px) {
#list { grid-template-columns: repeat(2, 1fr); }
}
.item {
cursor: pointer;
color: #404040;
background: #f2f2f2;
}
.iImg {
object-fit: contain;
width: 100%;
background: #f6f6f6;
}
.iName, .iPrice {
padding: 5px;
font-weight: 700;
}
.iPrice { color: #6a7bf1; }
/* (C) CART */
#cart { color: #fff; }
.cDel, #cart input[type=button] { cursor: pointer; }
.cRow {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #dfdfdf;
}
.cDel {
color: #c92f2f;
font-size: 24px;
margin-right: 5px;
}
.cItem {
flex-grow: 1;
padding: 5px;
}
.cName { font-weight: 700; }
.cQty {
width: 40px;
padding: 10px 5px;
text-align: center;
border: 1px solid #aaa;
}
.cTotal, .cAmt {
font-weight: 700;
padding: 10px 0;
}
.cTotal { flex-grow: 1; }
#cart input[type=button] {
width: 45%;
padding: 10px;
margin: 20px 5px 0 5px;
border: 0;
font-weight: 700;
color: #fff;
background: #1852d5;
}
{
"short_name": "POS",
"name": "PHP POS",
"icons": [{
"src": "assets/favicon.png",
"sizes": "64x64",
"type": "image/png"
}, {
"src": "assets/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}],
"start_url": "4a-pos.html",
"scope": "/",
"background_color": "white",
"theme_color": "white",
"display": "standalone"
}
// (A) CREATE/INSTALL CACHE
self.addEventListener("install", evt => {
self.skipWaiting();
evt.waitUntil(
caches.open("PHPPOS")
.then(cache => cache.addAll([
"4a-pos.html",
"4b-pos.js",
"4c-cart.js",
"4d-pos.css",
"5a-manifest.json",
"assets/favicon.png",
"assets/icon-512.png"
]))
.catch(err => console.error(err))
);
});
// (B) CLAIM CONTROL INSTANTLY
self.addEventListener("activate", evt => self.clients.claim());
// (C) LOAD FROM CACHE FIRST, FALLBACK TO NETWORK IF NOT FOUND
self.addEventListener("fetch", evt => evt.respondWith(
caches.match(evt.request).then(res => res || fetch(evt.request))
));
md assets
curl https://user-images.githubusercontent.com/11156244/239247626-91561738-be24-4822-be36-71cb5067fd0f.png --ssl-no-revoke --output assets/favicon.png
curl https://user-images.githubusercontent.com/11156244/239247649-43dee2cc-7065-470e-821f-77e7378bbd1e.png --ssl-no-revoke --output assets/icon-512.png
curl https://user-images.githubusercontent.com/11156244/282650904-05dac31a-97c4-4e04-acd2-4529afb4b9e9.png --ssl-no-revoke --output assets/img-a.png
curl https://user-images.githubusercontent.com/11156244/282650909-ff5fb791-01b2-459b-98fe-7a5d02190cda.png --ssl-no-revoke --output assets/img-b.png
curl https://user-images.githubusercontent.com/11156244/282650911-8303db6b-12b5-4f39-ba4a-934d9d235854.png --ssl-no-revoke --output assets/img-c.png
curl https://user-images.githubusercontent.com/11156244/282650914-99750227-8ea7-4675-9ab6-cd29b56737c0.png --ssl-no-revoke --output assets/img-d.png
curl https://user-images.githubusercontent.com/11156244/282650917-f579d703-4bf8-4822-97b2-f65cb02a8279.png --ssl-no-revoke --output assets/img-e.png
curl https://user-images.githubusercontent.com/11156244/282650920-591ec336-c020-4da9-b80b-fae13d317dcb.png --ssl-no-revoke --output assets/img-f.png
mkdir -m 777 assets
curl https://user-images.githubusercontent.com/11156244/239247626-91561738-be24-4822-be36-71cb5067fd0f.png --ssl-no-revoke --output ./assets/favicon.png
curl https://user-images.githubusercontent.com/11156244/239247649-43dee2cc-7065-470e-821f-77e7378bbd1e.png --ssl-no-revoke --output ./assets/icon-512.png
curl https://user-images.githubusercontent.com/11156244/282650904-05dac31a-97c4-4e04-acd2-4529afb4b9e9.png --ssl-no-revoke --output ./assets/img-a.png
curl https://user-images.githubusercontent.com/11156244/282650909-ff5fb791-01b2-459b-98fe-7a5d02190cda.png --ssl-no-revoke --output ./assets/img-b.png
curl https://user-images.githubusercontent.com/11156244/282650911-8303db6b-12b5-4f39-ba4a-934d9d235854.png --ssl-no-revoke --output ./assets/img-c.png
curl https://user-images.githubusercontent.com/11156244/282650914-99750227-8ea7-4675-9ab6-cd29b56737c0.png --ssl-no-revoke --output ./assets/img-d.png
curl https://user-images.githubusercontent.com/11156244/282650917-f579d703-4bf8-4822-97b2-f65cb02a8279.png --ssl-no-revoke --output ./assets/img-e.png
curl https://user-images.githubusercontent.com/11156244/282650920-591ec336-c020-4da9-b80b-fae13d317dcb.png --ssl-no-revoke --output ./assets/img-f.png
<?php define('POS_UPDATE', 1659061930);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment