Browse Source

Implement in-person transaction flow (close #7)

master
Skylar Ittner 1 year ago
parent
commit
aff3a068b1
14 changed files with 718 additions and 175 deletions
  1. 173
    0
      action.php
  2. BIN
      database.mwb
  3. 14
    1
      lang/en_us.php
  4. 6
    0
      pages.php
  5. 61
    2
      pages/pos.php
  6. 2
    2
      required.php
  7. 3
    0
      settings.template.php
  8. 0
    14
      static/css/app.css
  9. 28
    0
      static/css/pos.css
  10. 43
    0
      static/js/bsalert.js
  11. 28
    156
      static/js/pos.js
  12. 82
    0
      static/js/pos_finish.js
  13. 167
    0
      static/js/pos_items.js
  14. 111
    0
      static/js/pos_payment.js

+ 173
- 0
action.php View File

@@ -8,6 +8,7 @@
* Make things happen when buttons are pressed and forms submitted.
*/
require_once __DIR__ . "/required.php";
require_once __DIR__ . "/lib/userinfo.php";

if ($VARS['action'] !== "signout") {
dieifnotloggedin();
@@ -30,6 +31,166 @@ function returnToSender($msg, $arg = "") {
}

switch ($VARS['action']) {
case "finish_transaction":
header("Content-Type: application/json");
$items = $VARS['items'];
$payments = $VARS['payments'];
$customer = $VARS['customer'];
$register = $VARS['register'];

if ($customer != "" && !$database->has('customers', ['customerid' => $customer])) {
exit(json_encode(["status" => "ERROR", "message" => lang("invalid customer", false)]));
// exit(json_encode(["status" => "ERROR", "message" => lang("", false)]));
}
if ($register != "" && !$database->has('registers', ['registerid' => $register])) {
exit(json_encode(["status" => "ERROR", "message" => lang("invalid register", false)]));
}
if ($register != "" && !$database->has('cash_drawer', ['AND' => ['registerid' => $register, 'close' => null]])) {
exit(json_encode(["status" => "ERROR", "message" => lang("cash not open", false)]));
}

$totalcharge = 0.00;
$totalpaid = 0.00;
foreach ($items as $i) {
$totalcharge += $i['each'] * $i['qty'];
if (!$binstack->has('items', ['itemid' => $i['id']])) {
exit(json_encode(["status" => "ERROR", "message" => lang("invalid item", false)]));
}
}
foreach ($payments as $p) {
if (!$database->has('payment_types', ['typename' => $p['type']])) {
exit(json_encode(["status" => "ERROR", "message" => lang("invalid payment type", false)]));
}
$totalpaid += $p['amount'];
if ($p['type'] == "giftcard") {
if (!$database->has('certificates', ['AND' => ['amount[>=]' => $p['amount'], 'deleted[!]' => 1, 'certcode' => $p['code']]])) {
exit(json_encode(["status" => "ERROR", "message" => lang("invalid giftcard", false)]));
}
}
}

if ($totalcharge > $totalpaid) {
exit(json_encode(["status" => "ERROR", "message" => lang("insufficient payment", false)]));
}

$cashid = null;
if ($register != "") {
$cashid = $database->get('cash_drawer', 'cashid', ['AND' => ['registerid' => $register, 'close' => null]]);
}

$database->insert('transactions', [
'txdate' => date('Y-m-d H:i:s'),
'customerid' => ($customer != "" ? $customer : null),
'type' => 1,
'cashier' => $_SESSION['uid'],
'cashid' => $cashid
]);
$txid = $database->id();

foreach ($items as $i) {
$itemname = $binstack->get('items', 'name', ['itemid' => $i['id']]);
$database->insert('lines', [
'txid' => $txid,
'amount' => $i['each'],
'name' => $itemname,
'itemid' => $i['id'],
'qty' => $i['qty']
]);
}

foreach ($payments as $p) {
$certid = null;
if ($p['type'] == "giftcard") {
$certid = $database->get('certificates', 'certid', ['certcode' => $p['code']]);
}
$type = $database->get('payment_types', 'typeid', ['typename' => $p['type']]);
$database->insert('payments', [
'amount' => $p['amount'],
'data' => '',
'type' => $type,
'txid' => $txid,
'certid' => $certid
]);
}

exit(json_encode(["status" => "OK", "txid" => $txid]));

break;
case "getreceipt":
header("Content-Type: text/html");
if (!$database->has('transactions', ['txid' => $VARS['txid']])) {
exit(json_encode(["status" => "ERROR", "txid" => null]));
}

$tx = $database->get('transactions', ['txid', 'txdate', 'customerid', 'type', 'cashier'], ['txid' => $VARS['txid']]);

$txid = $tx['txid'];
$datetime = date(DATETIME_FORMAT, strtotime($tx['txdate']));
$type = $tx['type'];
$cashier = getUserByID($tx['cashier'])['name'];
$customerid = $tx['customerid'];
$customerline = (is_null($customerid) ? "" : "<br />Customer: $customerid");

$itemhtml = "";
$items = $database->select('lines', ['amount', 'name', 'itemid', 'qty'], ['txid' => $txid]);
$total = 0.0;
foreach ($items as $i) {
$itemhtml .= "\n";
$itemhtml .= '<div class="flexrow">';
$itemhtml .= '<div>' . $i['name'] . '</div>';
$itemhtml .= '<div>$' . $i['amount'] . '</div>';
$itemhtml .= '<div>x' . $i['qty'] . '</div>';
$itemhtml .= '<div>$' . ($i['qty'] * $i['amount']) . '</div>';
$itemhtml .= '</div>';
$total += ($i['qty'] * $i['amount']);
}

$paymenthtml = "";
$payments = $database->select('payments', [
'[>]payment_types' => ['type' => 'typeid']
], [
'amount', 'type', 'typename', 'text'
], [
'txid' => $txid
]);
foreach ($payments as $p) {
$paymenthtml .= "\n";
$paymenthtml .= '<div class="flexrow">';
$paymenthtml .= '<div>' . lang($p['text'], false) . '</div>';
$paymenthtml .= '<div>$' . $p['amount'] . '</div>';
$paymenthtml .= '</div>';
}


$html = <<<END
<!DOCTYPE html>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tx #$txid</title>
<style nonce="$SECURE_NONCE">
.flexrow {
display: flex;
justify-content: space-between;
}
</style>
<hr />
Date: $datetime<br />
Tx. ID: $txid<br />
Cashier: $cashier
$customerline
<hr />
<div id="items">
$itemhtml
</div>
<hr />
<div id="payments">
$paymenthtml
</div>
<hr />
<b>Total: $$total</b>
END;
exit($html);
break;
case "itemsearch":
header("Content-Type: application/json");
if (!is_empty($VARS['q'])) {
@@ -52,6 +213,18 @@ switch ($VARS['action']) {
], $where);
$items = (count($items) > 0 ? $items : false);
exit(json_encode(["status" => "OK", "items" => $items]));
case "giftcard_lookup":
header("Content-Type: application/json");
$code = $VARS['code'];
if (empty($code)) {
exit(json_encode(["status" => "ERROR", "cards" => []]));
}
$cards = $database->select('certificates', ['certid (id)', 'certcode (code)', 'amount (balance)', 'start_amount (amount)'], ['certcode' => $code]);
exit(json_encode(["status" => "OK", "cards" => $cards]));
break;
case "session_keepalive":
header("Content-Type: application/json");
exit(json_encode(["status" => "OK"]));
case "signout":
session_destroy();
header('Location: index.php');

BIN
database.mwb View File


+ 14
- 1
lang/en_us.php View File

@@ -31,5 +31,18 @@ define("STRINGS", [
"home" => "Home",
"point of sale" => "Point of Sale",
"barcode" => "Barcode",
"barcode or search" => "Barcode or Search"
"barcode or search" => "Barcode or Search",
"cash" => "Cash",
"check" => "Check",
"card" => "Card",
"crypto" => "Crypto",
"gift card" => "Gift Card",
"free" => "Free",
"paid" => "Paid",
"owed" => "Owed",
"change" => "Change",
"enter payment" => "Enter Payment",
"receipt" => "Receipt",
"close" => "Close",
"print" => "Print",
]);

+ 6
- 0
pages.php View File

@@ -15,8 +15,14 @@ define("PAGES", [
"title" => "point of sale",
"navbar" => true,
"icon" => "far fa-money-bill-alt",
"styles" => [
"static/css/pos.css",
],
"scripts" => [
"static/js/bsalert.js",
"static/js/pos_items.js",
"static/js/pos_payment.js",
"static/js/pos_finish.js",
"static/js/pos.js",
]
],

+ 61
- 2
pages/pos.php View File

@@ -5,6 +5,26 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
?>
<div class="modal" tabindex="-1" role="dialog" id="receiptmodal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><?php lang("receipt"); ?></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<iframe class="w-100" id="receiptframe"></iframe>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal"><?php lang("close"); ?></button>
<button type="button" class="btn btn-primary" id="receiptprintbtn"><i class="fas fa-print"></i> <?php lang("print"); ?></button>
</div>
</div>
</div>
</div>

<div class="row">
<div class="col-12 col-md-6 order-1 order-md-0">
<div class="card d-flex">
@@ -20,7 +40,7 @@
</div>
</div>
<div>
<div class="list-group" id="pos-lines-box">
<div class="list-group list-group-flush" id="pos-lines-box">
<!-- Items go here -->
</div>
</div>
@@ -29,7 +49,46 @@

<div class="col-12 col-md-6 order-0 order-md-1">
<div class="card mb-3 mb-md-0">
<div class="display-4 p-1 p-md-3 text-center">$<span class="grand-total">0.00</span></div>
<div class="display-4 p-1 p-md-3 text-center">$<span id="grand-total">0.00</span></div>
<div class="card-body">
<span class="btn btn-green btn-lg btn-block" id="paymentbtn"><i class="fas fa-money-bill-wave"></i> <?php lang("enter payment"); ?></span>
</div>
<div class="d-none" id="paymentui">
<div class="card-body">
<div class="d-flex justify-content-around flex-wrap">
<?php
$payment_methods = $database->select('payment_types', ['typeid (id)', 'typename (name)', 'icon', 'text']);
foreach ($payment_methods as $data) {
?>
<div class="card p-2 text-center m-1 payment-method-button" data-payment-method="<?php echo $data['name']; ?>" data-icon="<?php echo $data['icon']; ?>" data-text="<?php lang($data['text']); ?>">
<i class="<?php echo $data['icon']; ?> fa-3x fa-fw"></i>
<?php lang($data['text']); ?>
</div>
<?php
}
?>
</div>
</div>
<hr />
<div class="row px-2 mb-3 text-center">
<div class="col-12 col-sm-4">
<?php lang("paid"); ?> $<span id="paid-amount">0.00</span>
</div>
<div class="col-12 col-sm-4">
<?php lang("owed"); ?> $<span id="owed-amount">0.00</span>
</div>
<div class="col-12 col-sm-4">
<?php lang("change"); ?> $<span id="change-amount">0.00</span>
</div>
</div>
<div class="list-group list-group-flush" id="payment-lines">
<!-- Payments go here -->
</div>

<div class="card-body">
<span class="btn btn-green btn-lg btn-block" id="finishbtn"><i class="fas fa-receipt"></i> <?php lang("finish"); ?></span>
</div>
</div>
</div>
</div>
</div>

+ 2
- 2
required.php View File

@@ -39,7 +39,7 @@ if ($_SESSION['mobile'] === TRUE) {
. "object-src 'none'; "
. "img-src * data:; "
. "media-src 'self'; "
. "frame-src 'none'; "
. "frame-src 'self'; "
. "font-src 'self'; "
. "connect-src *; "
. "style-src 'self' 'unsafe-inline' $captcha_server; "
@@ -50,7 +50,7 @@ if ($_SESSION['mobile'] === TRUE) {
. "object-src 'none'; "
. "img-src * data:; "
. "media-src 'self'; "
. "frame-src 'none'; "
. "frame-src 'self'; "
. "font-src 'self'; "
. "connect-src *; "
. "style-src 'self' 'nonce-$SECURE_NONCE' $captcha_server; "

+ 3
- 0
settings.template.php View File

@@ -40,6 +40,9 @@ define("PORTAL_KEY", "123");
// For supported values, see http://php.net/manual/en/timezones.php
define("TIMEZONE", "America/Denver");

define("DATETIME_FORMAT", "M j Y g:i A"); // 12 hour time
#define("DATETIME_FORMAT", "M j Y G:i"); // 24 hour time

// Base URL for site links.
define('URL', '.');


+ 0
- 14
static/css/app.css View File

@@ -52,18 +52,4 @@ body {
.footer {
margin-top: 10em;
text-align: center;
}

input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
}

#pos-lines-box {
max-height: calc(100vh - 200px);
overflow-y: scroll;
}

+ 28
- 0
static/css/pos.css View File

@@ -0,0 +1,28 @@
/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
}

#pos-lines-box {
max-height: calc(100vh - 150px);
overflow-y: auto;
}

.payment-method-button {
cursor: pointer;
}

#receiptframe {
height: 50vh;
border: 0;
}

+ 43
- 0
static/js/bsalert.js View File

@@ -4,6 +4,49 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

function bsalert(title, message, okbtn, cancelbtn, callback) {
var html = '<div class="modal fade" id="bsalert">'
+ ' <div class="modal-dialog">'
+ ' <div class="modal-content">'
+ ' <div class="modal-header">'
+ ' <h5 class="modal-title" id="bsalert-title"></h5>'
+ ' <button type="button" class="close" data-dismiss="modal" aria-label="Close">'
+ ' <span aria-hidden="true">&times;</span>'
+ ' </button>'
+ ' </div>'
+ ' <div class="modal-body" id="bsalert-body">'
+ ' <div id="bsalert-message"></div>'
+ ' </div>'
+ ' <div class="modal-footer">'
+ ' <button type="button" class="btn btn-secondary" data-dismiss="modal" id="bsalert-cancel">Cancel</button>'
+ ' <button type="button" class="btn btn-primary" id="bsalert-ok">OK</button>'
+ ' </div>'
+ ' </div>'
+ ' </div>'
+ '</div>';
$("body").append(html);
$("#bsalert-title").text(title);
$("#bsalert-message").text(message);
if (typeof okbtn != "string") {
okbtn = "OK";
}
$("#bsalert-ok").text(okbtn);
if (typeof cancelbtn != "string") {
cancelbtn = "Cancel";
}
$("#bsalert-cancel").text(cancelbtn);
$("#bsalert-ok").on("click", function () {
if (typeof callback != 'undefined') {
callback();
}
$("#bsalert").modal("hide");
});
$("#bsalert").on("hidden.bs.modal", function () {
$("#bsalert").remove();
});
$("#bsalert").modal("show");
}

function bsprompt(title, message, okbtn, cancelbtn, type, callback) {
var html = '<div class="modal fade" id="bsprompt">'
+ ' <div class="modal-dialog">'

+ 28
- 156
static/js/pos.js View File

@@ -4,121 +4,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

function addItem(name, code, price) {
if ($(".list-group-item[data-code='" + code + "']").length) {
updateQty($(".list-group-item[data-code='" + code + "']").find(".qty-plus"), 1);
return;
}
price = (price * 1.0).toFixed(2);
$("#pos-lines-box").append('<div class="list-group-item" data-code="' + code + '">'
+ '<div class="d-flex w-100 justify-content-between mb-2">'
+ '<h5 class="item-name">'
+ name
+ '</h5>'
+ '<h5>'
+ '<small class="item-code mr-1">' + code + '</small>'
+ '<span class="badge badge-light">'
+ '$<span class="line-total">'
+ price
+ '</span>'
+ '</span>'
+ '</h5>'
+ '</div>'
+ '<div class="d-inline-flex">'
+ '<div class="input-group qty-control">'
+ '<div class="input-group-prepend">'
+ '<span class="input-group-text pr-1"><b>$</b></span>'
+ '</div>'
+ '<input type="money" class="form-control item-price" value="' + price + '"/>'
+ '<div class="input-group-prepend">'
+ '<span class="input-group-text px-2"><i class="fas fa-times"></i></span>'
+ '<button class="btn btn-red qty-minus" type="button"><i class="fas fa-trash"></i></button>'
+ '</div>'
+ '<input type="number" class="form-control item-qty px-2" value="1" />'
+ '<div class="input-group-append">'
+ '<button class="btn btn-light-green qty-plus" type="button"><i class="fas fa-plus"></i></button>'
+ '</div>'
+ '</div>'
+ '</div>'
+ '</div>');
recalculate();
}

function findItem(q) {
function decodeThenAddItem(item) {
var code = item.code1;
console.log(code);
if (code == "" && item["code2"] != "") {
code = item["code2"];
} else if (code == "") {
code = "---";
}
var price = item['price'];
if (price == null || price == "" || price == 0) {
if (!$(".list-group-item[data-code='" + code + "']").length) {
bsprompt("Enter Price",
"No price set. Enter a price for this item:",
"Add Item",
"Cancel",
"number",
function (result) {
addItem(item['name'], code, result);
});
return;
}
}
addItem(item['name'], code, price);
}
if (q == "") {
return;
}
$.get("action.php", {
action: "itemsearch",
q: q
}, function (data) {
if (data['items'].length == 1) {
decodeThenAddItem(data['items'][0]);
} else if (data['items'].length > 1) {
var options = [];
for (var i = 0; i < data['items'].length; i++) {
var text = data['items'][i]['name'];
if (data['items'][i]['price'] != null) {
text += " <span class=\"ml-auto\">$" + data['items'][i]['price'] + "</span>";
}
options.push({"text": text, "val": data['items'][i]['id']});
}
bschoices(
"Multiple Results",
"More than one item match the query. Pick the correct one:",
options,
"Cancel",
function (result) {
for (var i = 0; i < data['items'].length; i++) {
if (data['items'][i]['id'] == result) {
decodeThenAddItem(data['items'][i]);
break;
}
}
}
);
}
}).fail(function () {
alert("Error");
});
}

function removezero() {
$("#pos-lines-box .list-group-item").each(function () {
var qty = $(".item-qty", this).val() * 1.0;
if (qty == 0) {
$(this).remove();
}
});
}

function recalculate() {
removezero();
var total = 0.0;
var paid = 0.0;
var remaining = 0.0;
var change = 0.0;
$("#pos-lines-box .list-group-item").each(function () {
var each = $(".item-price", this).val() * 1.0;
var qty = $(".item-qty", this).val() * 1.0;
@@ -128,56 +19,34 @@ function recalculate() {
console.log(each.toFixed(2));
total += line;
});
$(".grand-total").text(total.toFixed(2));
}

function updateQty(btn, diff) {
var qtybox = $(btn).parent().parent().find(".item-qty");
var qty = parseInt(qtybox.val());
qty += diff;
if (qty > 0) {
qtybox.val(qty);
var minbtn = $(btn).parent().parent().find(".qty-minus");
if (qty == 1) {
minbtn.html("<i class=\"fas fa-trash\"></i>");
} else {
minbtn.html("<i class=\"fas fa-minus\"></i>");
}
$("#payment-lines .list-group-item").each(function () {
var line = $(".payment-entry", this).val() * 1.0;
paid += line;
});

remaining = total - paid;
change = (total - paid) * -1.0;
if (remaining <= 0) {
remaining = 0.0;
$("#owed-amount").parent().removeClass("font-weight-bold");
} else {
qtybox.closest(".list-group-item").remove();
$("#owed-amount").parent().addClass("font-weight-bold");
}
recalculate();
}

$("#pos-lines-box").on("click", ".qty-minus", function () {
updateQty(this, -1);
});

$("#pos-lines-box").on("click", ".qty-plus", function () {
updateQty(this, 1);
});

$("#pos-lines-box").on("change", ".item-qty,.item-price", function () {
recalculate();
});

$("#pos-lines-box").on("keypress", ".item-qty,.item-price", function (e) {
if (e.which === 13) {
recalculate();
if (change <= 0) {
change = 0.0;
$("#change-amount").parent().removeClass("font-weight-bold");
} else {
$("#change-amount").parent().addClass("font-weight-bold");
}
});

$("#barcode").on('keypress', function (e) {
if (e.which === 13) {
findItem($("#barcode").val());
$("#barcode").val("");
}
});
$("#grand-total").text(total.toFixed(2));
$("#paid-amount").text(paid.toFixed(2));
$("#owed-amount").text(remaining.toFixed(2));
$("#change-amount").text(change.toFixed(2));
}

$("#barcodebtn").on("click", function () {
findItem($("#barcode").val());
$("#barcode").val("");
});

$("body").on("keypress", "input[type=money],input[type=number]", function (e) {
//console.log(String.fromCharCode(e.which) + "|" + e.which);
@@ -193,4 +62,7 @@ $("body").on("keypress", "input[type=money],input[type=number]", function (e) {
}
});

$("#barcode").focus();
// Make sure the session doesn't expire
setInterval(function () {
$.get("action.php", {action: "session_keepalive"});
}, 1000 * 60);

+ 82
- 0
static/js/pos_finish.js View File

@@ -0,0 +1,82 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

function sendTransactionToServer(callback) {
var items = [];
var payments = [];
var customer = '';
var register = '';
$("#pos-lines-box .list-group-item").each(function () {
var each = $(".item-price", this).val() * 1.0;
var qty = $(".item-qty", this).val() * 1.0;
var code = $(this).data("code");
var id = $(this).data("itemid");
items.push({
each: each,
qty: qty,
code: code,
id: id
});
});
$("#payment-lines .list-group-item").each(function () {
var amount = $(".payment-entry", this).val() * 1.0;
var type = $(".payment-entry", this).data("type");
var code = '';
if ($(".giftcard-number", this).length) {
code = $(".giftcard-number", this).val();
}
payments.push({
amount: amount,
type: type,
code: code
});
});

$.post("action.php", {
action: "finish_transaction",
items: items,
payments: payments,
customer: customer,
register: register
}, function (data) {
if (data.status == "OK") {
callback(data);
} else {
bsalert("Error", "The transaction could not be completed:<br />" + data.message);
}
}).fail(function () {
bsalert("Error", "The transaction could not be completed due to an unknown error.");
});
}

function showReceipt(txid) {
$("#receiptframe").attr("src", 'action.php?action=getreceipt&txid=' + txid);
$("#receiptmodal").modal();
}

function finishTransaction() {
sendTransactionToServer(function (data) {
showReceipt(data.txid);
});
}

$("#finishbtn").click(function () {
recalculate();
var owed = $("#owed-amount").text() * 1.0;
if (owed > 0) {
bsalert("Incomplete Transaction", "The customer still owes $" + owed.toFixed(2) + ". Add a payment or remove items until everything is paid for.");
} else {
finishTransaction();
}
});

$("#receiptprintbtn").click(function () {
document.getElementById("receiptframe").contentWindow.print();
})

$("#receiptmodal").on("hide.bs.modal", function () {
window.location.reload();
});

+ 167
- 0
static/js/pos_items.js View File

@@ -0,0 +1,167 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

function addItem(name, code, price, id) {
if ($(".list-group-item[data-code='" + code + "']").length) {
updateQty($(".list-group-item[data-code='" + code + "']").find(".qty-plus"), 1);
return;
}
price = (price * 1.0).toFixed(2);
$("#pos-lines-box").append('<div class="list-group-item" data-code="' + code + '" data-itemid="' + id + '">'
+ '<div class="d-flex w-100 justify-content-between mb-2">'
+ '<h5 class="item-name">'
+ name
+ '</h5>'
+ '<h5>'
+ '<small class="item-code mr-1">' + code + '</small>'
+ '<span class="badge badge-light">'
+ '$<span class="line-total">'
+ price
+ '</span>'
+ '</span>'
+ '</h5>'
+ '</div>'
+ '<div class="d-inline-flex">'
+ '<div class="input-group qty-control">'
+ '<div class="input-group-prepend">'
+ '<span class="input-group-text pr-1"><b>$</b></span>'
+ '</div>'
+ '<input type="money" class="form-control item-price" value="' + price + '"/>'
+ '<div class="input-group-prepend">'
+ '<span class="input-group-text px-2"><i class="fas fa-times"></i></span>'
+ '<button class="btn btn-red qty-minus" type="button"><i class="fas fa-trash"></i></button>'
+ '</div>'
+ '<input type="number" class="form-control item-qty px-2" value="1" />'
+ '<div class="input-group-append">'
+ '<button class="btn btn-light-green qty-plus" type="button"><i class="fas fa-plus"></i></button>'
+ '</div>'
+ '</div>'
+ '</div>'
+ '</div>');
recalculate();
}

function findItem(q) {
function decodeThenAddItem(item) {
var code = item.code1;
console.log(code);
if (code == "" && item["code2"] != "") {
code = item["code2"];
} else if (code == "") {
code = "---";
}
var price = item['price'];
if (price == null || price == "" || price == 0) {
if (!$(".list-group-item[data-code='" + code + "']").length) {
bsprompt("Enter Price",
"No price set. Enter a price for this item:",
"Add Item",
"Cancel",
"number",
function (result) {
addItem(item['name'], code, result, item['id']);
});
return;
}
}
addItem(item['name'], code, price, item['id']);
}
if (q == "") {
return;
}
$.get("action.php", {
action: "itemsearch",
q: q
}, function (data) {
if (data['items'].length == 1) {
decodeThenAddItem(data['items'][0]);
} else if (data['items'].length > 1) {
var options = [];
for (var i = 0; i < data['items'].length; i++) {
var text = data['items'][i]['name'];
if (data['items'][i]['price'] != null) {
text += " <span class=\"ml-auto\">$" + data['items'][i]['price'] + "</span>";
}
options.push({"text": text, "val": data['items'][i]['id']});
}
bschoices(
"Multiple Results",
"More than one item match the query. Pick the correct one:",
options,
"Cancel",
function (result) {
for (var i = 0; i < data['items'].length; i++) {
if (data['items'][i]['id'] == result) {
decodeThenAddItem(data['items'][i]);
break;
}
}
}
);
}
}).fail(function () {
alert("Error");
});
}

function removezero() {
$("#pos-lines-box .list-group-item").each(function () {
var qty = $(".item-qty", this).val() * 1.0;
if (qty == 0) {
$(this).remove();
}
});
}

function updateQty(btn, diff) {
var qtybox = $(btn).parent().parent().find(".item-qty");
var qty = parseInt(qtybox.val());
qty += diff;
if (qty > 0) {
qtybox.val(qty);
var minbtn = $(btn).parent().parent().find(".qty-minus");
if (qty == 1) {
minbtn.html("<i class=\"fas fa-trash\"></i>");
} else {
minbtn.html("<i class=\"fas fa-minus\"></i>");
}
} else {
qtybox.closest(".list-group-item").remove();
}
recalculate();
}

$("#pos-lines-box").on("click", ".qty-minus", function () {
updateQty(this, -1);
});

$("#pos-lines-box").on("click", ".qty-plus", function () {
updateQty(this, 1);
});

$("#pos-lines-box").on("change blur", ".item-qty,.item-price", function () {
recalculate();
});

$("#pos-lines-box").on("keypress", ".item-qty,.item-price", function (e) {
if (e.which === 13) {
recalculate();
}
});

$("#barcode").on('keypress', function (e) {
if (e.which === 13) {
findItem($("#barcode").val());
$("#barcode").val("");
}
});

$("#barcodebtn").on("click", function () {
findItem($("#barcode").val());
$("#barcode").val("");
});

$("#barcode").focus();

+ 111
- 0
static/js/pos_payment.js View File

@@ -0,0 +1,111 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

function addPayment(type, icon, text) {
var extrafield = "";
if (type == "giftcard") {
extrafield = ''
+ ' <div class="input-group-prepend input-group-append">'
+ ' <span class="input-group-text">'
+ ' #'
+ ' </span>'
+ ' </div>'
+ ' <input class="form-control giftcard-number" type="number" />';
}
var amount = "";
// Autofill the exact due amount for payment methods that are flexible like that
if (type == "check" || type == "card" || type == "crypto") {
if ($("#owed-amount").text() * 1.0 > 0) {
amount = ($("#owed-amount").text() * 1.0).toFixed(2);
}
}
$("#payment-lines").append(''
+ '<div class="list-group-item">'
+ ' <div class="input-group">'
+ ' <div class="input-group-prepend">'
+ ' <span class="input-group-text">'
+ ' <i class="' + icon + ' fa-fw mr-1"></i> '
+ text
+ ' </span>'
+ ' </div>'
+ ' <div class="input-group-prepend">'
+ ' <span class="input-group-text">'
+ ' $'
+ ' </span>'
+ ' </div>'
+ ' <input class="form-control payment-entry" type="money" data-type="' + type + '" value="' + amount + '" />'
+ extrafield
+ ' <div class="input-group-append">'
+ ' <span class="btn btn-outline-danger remove-payment-btn">'
+ ' <i class="fas fa-trash"></i>'
+ ' </span>'
+ ' </div>'
+ ' </div>'
+ '</div>');
$("#payment-lines .payment-entry").last().focus();
}

function checkGiftCardBalance() {
$("#payment-lines .list-group-item:has(.giftcard-number)").each(function () {
var paymentbox = $(".payment-entry", this);
var cardnumberbox = $(".giftcard-number", this);
var amount = paymentbox.val() * 1.0;
var cardnumber = cardnumberbox.val();
if (cardnumber == "") {
return;
}
$.get("action.php", {
action: "giftcard_lookup",
code: cardnumber
}, function (json) {
if (json.status == "OK") {
if (json.cards.length == 0) {
bsalert("Invalid Gift Card", "Gift card #" + cardnumber + " does not exist. Remove it to finish the transaction.");
cardnumberbox.addClass('is-invalid');
} else if (json.cards.length == 1) {
if (json.cards[0].balance < amount) {
bsalert("Insufficient Gift Card Balance", "Gift card #" + cardnumber + " does not contain enough funds. The amount has been set to the full amount remaining on the card ($" + json.cards[0].balance + ").");
paymentbox.val(json.cards[0].balance);
}
cardnumberbox.removeClass('is-invalid');
} else {
bsalert("Invalid Gift Card", "Unable to determine which gift card #" + cardnumber + " is referring to. Remove it to finish the transaction.");
cardnumberbox.addClass('is-invalid');
}
} else {
bsalert("Invalid Gift Card", "Unable to determine which gift card #" + cardnumber + " is referring to. Remove it to finish the transaction.");
cardnumberbox.addClass('is-invalid');
}
});
});
}

$("#payment-lines").on("change keyup blur", ".payment-entry", function () {
recalculate();
});

$("#payment-lines").on("blur", ".giftcard-number,.payment-entry[data-type=giftcard]", function () {
checkGiftCardBalance();
});

$("#payment-lines").on("keypress", ".giftcard-number,.payment-entry[data-type=giftcard]", function (e) {
if (e.which === 13) {
checkGiftCardBalance();
}
});

$("#payment-lines").on("click", ".remove-payment-btn", function () {
$(this).closest(".list-group-item").remove();
recalculate();
});

$("#paymentbtn").click(function () {
$("#paymentui").removeClass("d-none");
});

$(".payment-method-button").click(function () {
addPayment($(this).data("payment-method"), $(this).data("icon"), $(this).data("text"));
});

Loading…
Cancel
Save