Browse Source

Implement in-person transaction flow (close #7)

master
Skylar Ittner 11 months 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 @@
8 8
  * Make things happen when buttons are pressed and forms submitted.
9 9
  */
10 10
 require_once __DIR__ . "/required.php";
11
+require_once __DIR__ . "/lib/userinfo.php";
11 12
 
12 13
 if ($VARS['action'] !== "signout") {
13 14
     dieifnotloggedin();
@@ -30,6 +31,166 @@ function returnToSender($msg, $arg = "") {
30 31
 }
31 32
 
32 33
 switch ($VARS['action']) {
34
+    case "finish_transaction":
35
+        header("Content-Type: application/json");
36
+        $items = $VARS['items'];
37
+        $payments = $VARS['payments'];
38
+        $customer = $VARS['customer'];
39
+        $register = $VARS['register'];
40
+
41
+        if ($customer != "" && !$database->has('customers', ['customerid' => $customer])) {
42
+            exit(json_encode(["status" => "ERROR", "message" => lang("invalid customer", false)]));
43
+            // exit(json_encode(["status" => "ERROR", "message" => lang("", false)]));
44
+        }
45
+        if ($register != "" && !$database->has('registers', ['registerid' => $register])) {
46
+            exit(json_encode(["status" => "ERROR", "message" => lang("invalid register", false)]));
47
+        }
48
+        if ($register != "" && !$database->has('cash_drawer', ['AND' => ['registerid' => $register, 'close' => null]])) {
49
+            exit(json_encode(["status" => "ERROR", "message" => lang("cash not open", false)]));
50
+        }
51
+
52
+        $totalcharge = 0.00;
53
+        $totalpaid = 0.00;
54
+        foreach ($items as $i) {
55
+            $totalcharge += $i['each'] * $i['qty'];
56
+            if (!$binstack->has('items', ['itemid' => $i['id']])) {
57
+                exit(json_encode(["status" => "ERROR", "message" => lang("invalid item", false)]));
58
+            }
59
+        }
60
+        foreach ($payments as $p) {
61
+            if (!$database->has('payment_types', ['typename' => $p['type']])) {
62
+                exit(json_encode(["status" => "ERROR", "message" => lang("invalid payment type", false)]));
63
+            }
64
+            $totalpaid += $p['amount'];
65
+            if ($p['type'] == "giftcard") {
66
+                if (!$database->has('certificates', ['AND' => ['amount[>=]' => $p['amount'], 'deleted[!]' => 1, 'certcode' => $p['code']]])) {
67
+                    exit(json_encode(["status" => "ERROR", "message" => lang("invalid giftcard", false)]));
68
+                }
69
+            }
70
+        }
71
+
72
+        if ($totalcharge > $totalpaid) {
73
+            exit(json_encode(["status" => "ERROR", "message" => lang("insufficient payment", false)]));
74
+        }
75
+
76
+        $cashid = null;
77
+        if ($register != "") {
78
+            $cashid = $database->get('cash_drawer', 'cashid', ['AND' => ['registerid' => $register, 'close' => null]]);
79
+        }
80
+
81
+        $database->insert('transactions', [
82
+            'txdate' => date('Y-m-d H:i:s'),
83
+            'customerid' => ($customer != "" ? $customer : null),
84
+            'type' => 1,
85
+            'cashier' => $_SESSION['uid'],
86
+            'cashid' => $cashid
87
+        ]);
88
+        $txid = $database->id();
89
+
90
+        foreach ($items as $i) {
91
+            $itemname = $binstack->get('items', 'name', ['itemid' => $i['id']]);
92
+            $database->insert('lines', [
93
+                'txid' => $txid,
94
+                'amount' => $i['each'],
95
+                'name' => $itemname,
96
+                'itemid' => $i['id'],
97
+                'qty' => $i['qty']
98
+            ]);
99
+        }
100
+
101
+        foreach ($payments as $p) {
102
+            $certid = null;
103
+            if ($p['type'] == "giftcard") {
104
+                $certid = $database->get('certificates', 'certid', ['certcode' => $p['code']]);
105
+            }
106
+            $type = $database->get('payment_types', 'typeid', ['typename' => $p['type']]);
107
+            $database->insert('payments', [
108
+                'amount' => $p['amount'],
109
+                'data' => '',
110
+                'type' => $type,
111
+                'txid' => $txid,
112
+                'certid' => $certid
113
+            ]);
114
+        }
115
+
116
+        exit(json_encode(["status" => "OK", "txid" => $txid]));
117
+
118
+        break;
119
+    case "getreceipt":
120
+        header("Content-Type: text/html");
121
+        if (!$database->has('transactions', ['txid' => $VARS['txid']])) {
122
+            exit(json_encode(["status" => "ERROR", "txid" => null]));
123
+        }
124
+
125
+        $tx = $database->get('transactions', ['txid', 'txdate', 'customerid', 'type', 'cashier'], ['txid' => $VARS['txid']]);
126
+
127
+        $txid = $tx['txid'];
128
+        $datetime = date(DATETIME_FORMAT, strtotime($tx['txdate']));
129
+        $type = $tx['type'];
130
+        $cashier = getUserByID($tx['cashier'])['name'];
131
+        $customerid = $tx['customerid'];
132
+        $customerline = (is_null($customerid) ? "" : "<br />Customer: $customerid");
133
+
134
+        $itemhtml = "";
135
+        $items = $database->select('lines', ['amount', 'name', 'itemid', 'qty'], ['txid' => $txid]);
136
+        $total = 0.0;
137
+        foreach ($items as $i) {
138
+            $itemhtml .= "\n";
139
+            $itemhtml .= '<div class="flexrow">';
140
+            $itemhtml .= '<div>' . $i['name'] . '</div>';
141
+            $itemhtml .= '<div>$' . $i['amount'] . '</div>';
142
+            $itemhtml .= '<div>x' . $i['qty'] . '</div>';
143
+            $itemhtml .= '<div>$' . ($i['qty'] * $i['amount']) . '</div>';
144
+            $itemhtml .= '</div>';
145
+            $total += ($i['qty'] * $i['amount']);
146
+        }
147
+
148
+        $paymenthtml = "";
149
+        $payments = $database->select('payments', [
150
+            '[>]payment_types' => ['type' => 'typeid']
151
+                ], [
152
+            'amount', 'type', 'typename', 'text'
153
+                ], [
154
+            'txid' => $txid
155
+        ]);
156
+        foreach ($payments as $p) {
157
+            $paymenthtml .= "\n";
158
+            $paymenthtml .= '<div class="flexrow">';
159
+            $paymenthtml .= '<div>' . lang($p['text'], false) . '</div>';
160
+            $paymenthtml .= '<div>$' . $p['amount'] . '</div>';
161
+            $paymenthtml .= '</div>';
162
+        }
163
+
164
+
165
+        $html = <<<END
166
+<!DOCTYPE html>
167
+<meta charset="UTF-8">
168
+<meta name="viewport" content="width=device-width, initial-scale=1">
169
+<title>Tx #$txid</title>
170
+<style nonce="$SECURE_NONCE">
171
+.flexrow {
172
+    display: flex;
173
+    justify-content: space-between;
174
+}
175
+</style>
176
+<hr />
177
+Date: $datetime<br />
178
+Tx. ID: $txid<br />
179
+Cashier: $cashier
180
+$customerline
181
+<hr />
182
+<div id="items">
183
+$itemhtml
184
+</div>
185
+<hr />
186
+<div id="payments">
187
+$paymenthtml
188
+</div>
189
+<hr />
190
+<b>Total: $$total</b>
191
+END;
192
+        exit($html);
193
+        break;
33 194
     case "itemsearch":
34 195
         header("Content-Type: application/json");
35 196
         if (!is_empty($VARS['q'])) {
@@ -52,6 +213,18 @@ switch ($VARS['action']) {
52 213
                 ], $where);
53 214
         $items = (count($items) > 0 ? $items : false);
54 215
         exit(json_encode(["status" => "OK", "items" => $items]));
216
+    case "giftcard_lookup":
217
+        header("Content-Type: application/json");
218
+        $code = $VARS['code'];
219
+        if (empty($code)) {
220
+            exit(json_encode(["status" => "ERROR", "cards" => []]));
221
+        }
222
+        $cards = $database->select('certificates', ['certid (id)', 'certcode (code)', 'amount (balance)', 'start_amount (amount)'], ['certcode' => $code]);
223
+        exit(json_encode(["status" => "OK", "cards" => $cards]));
224
+        break;
225
+    case "session_keepalive":
226
+        header("Content-Type: application/json");
227
+        exit(json_encode(["status" => "OK"]));
55 228
     case "signout":
56 229
         session_destroy();
57 230
         header('Location: index.php');

BIN
database.mwb View File


+ 14
- 1
lang/en_us.php View File

@@ -31,5 +31,18 @@ define("STRINGS", [
31 31
     "home" => "Home",
32 32
     "point of sale" => "Point of Sale",
33 33
     "barcode" => "Barcode",
34
-    "barcode or search" => "Barcode or Search"
34
+    "barcode or search" => "Barcode or Search",
35
+    "cash" => "Cash",
36
+    "check" => "Check",
37
+    "card" => "Card",
38
+    "crypto" => "Crypto",
39
+    "gift card" => "Gift Card",
40
+    "free" => "Free",
41
+    "paid" => "Paid",
42
+    "owed" => "Owed",
43
+    "change" => "Change",
44
+    "enter payment" => "Enter Payment",
45
+    "receipt" => "Receipt",
46
+    "close" => "Close",
47
+    "print" => "Print",
35 48
 ]);

+ 6
- 0
pages.php View File

@@ -15,8 +15,14 @@ define("PAGES", [
15 15
         "title" => "point of sale",
16 16
         "navbar" => true,
17 17
         "icon" => "far fa-money-bill-alt",
18
+        "styles" => [
19
+            "static/css/pos.css",
20
+        ],
18 21
         "scripts" => [
19 22
             "static/js/bsalert.js",
23
+            "static/js/pos_items.js",
24
+            "static/js/pos_payment.js",
25
+            "static/js/pos_finish.js",
20 26
             "static/js/pos.js",
21 27
         ]
22 28
     ],

+ 61
- 2
pages/pos.php View File

@@ -5,6 +5,26 @@
5 5
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 6
  */
7 7
 ?>
8
+<div class="modal" tabindex="-1" role="dialog" id="receiptmodal">
9
+    <div class="modal-dialog" role="document">
10
+        <div class="modal-content">
11
+            <div class="modal-header">
12
+                <h5 class="modal-title"><?php lang("receipt"); ?></h5>
13
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
14
+                    <span aria-hidden="true">&times;</span>
15
+                </button>
16
+            </div>
17
+            <div class="modal-body">
18
+                <iframe class="w-100" id="receiptframe"></iframe>
19
+            </div>
20
+            <div class="modal-footer">
21
+                <button type="button" class="btn btn-secondary" data-dismiss="modal"><?php lang("close"); ?></button>
22
+                <button type="button" class="btn btn-primary" id="receiptprintbtn"><i class="fas fa-print"></i> <?php lang("print"); ?></button>
23
+            </div>
24
+        </div>
25
+    </div>
26
+</div>
27
+
8 28
 <div class="row">
9 29
     <div class="col-12 col-md-6 order-1 order-md-0">
10 30
         <div class="card d-flex">
@@ -20,7 +40,7 @@
20 40
                 </div>
21 41
             </div>
22 42
             <div>
23
-                <div class="list-group" id="pos-lines-box">
43
+                <div class="list-group list-group-flush" id="pos-lines-box">
24 44
                     <!-- Items go here -->
25 45
                 </div>
26 46
             </div>
@@ -29,7 +49,46 @@
29 49
 
30 50
     <div class="col-12 col-md-6 order-0 order-md-1">
31 51
         <div class="card mb-3 mb-md-0">
32
-            <div class="display-4 p-1 p-md-3 text-center">$<span class="grand-total">0.00</span></div>
52
+            <div class="display-4 p-1 p-md-3 text-center">$<span id="grand-total">0.00</span></div>
53
+            <div class="card-body">
54
+                <span class="btn btn-green btn-lg btn-block" id="paymentbtn"><i class="fas fa-money-bill-wave"></i> <?php lang("enter payment"); ?></span>
55
+            </div>
56
+            <div class="d-none" id="paymentui">
57
+                <div class="card-body">
58
+                    <div class="d-flex justify-content-around flex-wrap">
59
+                        <?php
60
+                        $payment_methods = $database->select('payment_types', ['typeid (id)', 'typename (name)', 'icon', 'text']);
61
+                        foreach ($payment_methods as $data) {
62
+                            ?>
63
+                            <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']); ?>">
64
+                                <i class="<?php echo $data['icon']; ?> fa-3x fa-fw"></i>
65
+                                <?php lang($data['text']); ?>
66
+                            </div>
67
+                            <?php
68
+                        }
69
+                        ?>
70
+                    </div>
71
+                </div>
72
+                <hr />
73
+                <div class="row px-2 mb-3 text-center">
74
+                    <div class="col-12 col-sm-4">
75
+                        <?php lang("paid"); ?> $<span id="paid-amount">0.00</span>
76
+                    </div>
77
+                    <div class="col-12 col-sm-4">
78
+                        <?php lang("owed"); ?> $<span id="owed-amount">0.00</span>
79
+                    </div>
80
+                    <div class="col-12 col-sm-4">
81
+                        <?php lang("change"); ?> $<span id="change-amount">0.00</span>
82
+                    </div>
83
+                </div>
84
+                <div class="list-group list-group-flush" id="payment-lines">
85
+                    <!-- Payments go here -->
86
+                </div>
87
+
88
+                <div class="card-body">
89
+                    <span class="btn btn-green btn-lg btn-block" id="finishbtn"><i class="fas fa-receipt"></i> <?php lang("finish"); ?></span>
90
+                </div>
91
+            </div>
33 92
         </div>
34 93
     </div>
35 94
 </div>

+ 2
- 2
required.php View File

@@ -39,7 +39,7 @@ if ($_SESSION['mobile'] === TRUE) {
39 39
             . "object-src 'none'; "
40 40
             . "img-src * data:; "
41 41
             . "media-src 'self'; "
42
-            . "frame-src 'none'; "
42
+            . "frame-src 'self'; "
43 43
             . "font-src 'self'; "
44 44
             . "connect-src *; "
45 45
             . "style-src 'self' 'unsafe-inline' $captcha_server; "
@@ -50,7 +50,7 @@ if ($_SESSION['mobile'] === TRUE) {
50 50
             . "object-src 'none'; "
51 51
             . "img-src * data:; "
52 52
             . "media-src 'self'; "
53
-            . "frame-src 'none'; "
53
+            . "frame-src 'self'; "
54 54
             . "font-src 'self'; "
55 55
             . "connect-src *; "
56 56
             . "style-src 'self' 'nonce-$SECURE_NONCE' $captcha_server; "

+ 3
- 0
settings.template.php View File

@@ -40,6 +40,9 @@ define("PORTAL_KEY", "123");
40 40
 // For supported values, see http://php.net/manual/en/timezones.php
41 41
 define("TIMEZONE", "America/Denver");
42 42
 
43
+define("DATETIME_FORMAT", "M j Y g:i A"); // 12 hour time
44
+#define("DATETIME_FORMAT", "M j Y G:i"); // 24 hour time
45
+
43 46
 // Base URL for site links.
44 47
 define('URL', '.');
45 48
 

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

@@ -52,18 +52,4 @@ body {
52 52
 .footer {
53 53
     margin-top: 10em;
54 54
     text-align: center;
55
-}
56
-
57
-input[type="number"]::-webkit-outer-spin-button,
58
-input[type="number"]::-webkit-inner-spin-button {
59
-    -webkit-appearance: none;
60
-    margin: 0;
61
-}
62
-input[type="number"] {
63
-    -moz-appearance: textfield;
64
-}
65
-
66
-#pos-lines-box {
67
-    max-height: calc(100vh - 200px);
68
-    overflow-y: scroll;
69 55
 }

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

@@ -0,0 +1,28 @@
1
+/*
2
+This Source Code Form is subject to the terms of the Mozilla Public
3
+License, v. 2.0. If a copy of the MPL was not distributed with this
4
+file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+*/
6
+
7
+input[type="number"]::-webkit-outer-spin-button,
8
+input[type="number"]::-webkit-inner-spin-button {
9
+    -webkit-appearance: none;
10
+    margin: 0;
11
+}
12
+input[type="number"] {
13
+    -moz-appearance: textfield;
14
+}
15
+
16
+#pos-lines-box {
17
+    max-height: calc(100vh - 150px);
18
+    overflow-y: auto;
19
+}
20
+
21
+.payment-method-button {
22
+    cursor: pointer;
23
+}
24
+
25
+#receiptframe {
26
+    height: 50vh;
27
+    border: 0;
28
+}

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

@@ -4,6 +4,49 @@
4 4
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 5
  */
6 6
 
7
+function bsalert(title, message, okbtn, cancelbtn, callback) {
8
+    var html = '<div class="modal fade" id="bsalert">'
9
+            + '  <div class="modal-dialog">'
10
+            + '    <div class="modal-content">'
11
+            + '      <div class="modal-header">'
12
+            + '        <h5 class="modal-title" id="bsalert-title"></h5>'
13
+            + '        <button type="button" class="close" data-dismiss="modal" aria-label="Close">'
14
+            + '          <span aria-hidden="true">&times;</span>'
15
+            + '        </button>'
16
+            + '      </div>'
17
+            + '      <div class="modal-body" id="bsalert-body">'
18
+            + '        <div id="bsalert-message"></div>'
19
+            + '      </div>'
20
+            + '      <div class="modal-footer">'
21
+            + '        <button type="button" class="btn btn-secondary" data-dismiss="modal" id="bsalert-cancel">Cancel</button>'
22
+            + '        <button type="button" class="btn btn-primary" id="bsalert-ok">OK</button>'
23
+            + '      </div>'
24
+            + '    </div>'
25
+            + '  </div>'
26
+            + '</div>';
27
+    $("body").append(html);
28
+    $("#bsalert-title").text(title);
29
+    $("#bsalert-message").text(message);
30
+    if (typeof okbtn != "string") {
31
+        okbtn = "OK";
32
+    }
33
+    $("#bsalert-ok").text(okbtn);
34
+    if (typeof cancelbtn != "string") {
35
+        cancelbtn = "Cancel";
36
+    }
37
+    $("#bsalert-cancel").text(cancelbtn);
38
+    $("#bsalert-ok").on("click", function () {
39
+        if (typeof callback != 'undefined') {
40
+            callback();
41
+        }
42
+        $("#bsalert").modal("hide");
43
+    });
44
+    $("#bsalert").on("hidden.bs.modal", function () {
45
+        $("#bsalert").remove();
46
+    });
47
+    $("#bsalert").modal("show");
48
+}
49
+
7 50
 function bsprompt(title, message, okbtn, cancelbtn, type, callback) {
8 51
     var html = '<div class="modal fade" id="bsprompt">'
9 52
             + '  <div class="modal-dialog">'

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

@@ -4,121 +4,12 @@
4 4
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 5
  */
6 6
 
7
-function addItem(name, code, price) {
8
-    if ($(".list-group-item[data-code='" + code + "']").length) {
9
-        updateQty($(".list-group-item[data-code='" + code + "']").find(".qty-plus"), 1);
10
-        return;
11
-    }
12
-    price = (price * 1.0).toFixed(2);
13
-    $("#pos-lines-box").append('<div class="list-group-item" data-code="' + code + '">'
14
-            + '<div class="d-flex w-100 justify-content-between mb-2">'
15
-            + '<h5 class="item-name">'
16
-            + name
17
-            + '</h5>'
18
-            + '<h5>'
19
-            + '<small class="item-code mr-1">' + code + '</small>'
20
-            + '<span class="badge badge-light">'
21
-            + '$<span class="line-total">'
22
-            + price
23
-            + '</span>'
24
-            + '</span>'
25
-            + '</h5>'
26
-            + '</div>'
27
-            + '<div class="d-inline-flex">'
28
-            + '<div class="input-group qty-control">'
29
-            + '<div class="input-group-prepend">'
30
-            + '<span class="input-group-text pr-1"><b>$</b></span>'
31
-            + '</div>'
32
-            + '<input type="money" class="form-control item-price" value="' + price + '"/>'
33
-            + '<div class="input-group-prepend">'
34
-            + '<span class="input-group-text px-2"><i class="fas fa-times"></i></span>'
35
-            + '<button class="btn btn-red qty-minus" type="button"><i class="fas fa-trash"></i></button>'
36
-            + '</div>'
37
-            + '<input type="number" class="form-control item-qty px-2" value="1" />'
38
-            + '<div class="input-group-append">'
39
-            + '<button class="btn btn-light-green qty-plus" type="button"><i class="fas fa-plus"></i></button>'
40
-            + '</div>'
41
-            + '</div>'
42
-            + '</div>'
43
-            + '</div>');
44
-    recalculate();
45
-}
46
-
47
-function findItem(q) {
48
-    function decodeThenAddItem(item) {
49
-        var code = item.code1;
50
-        console.log(code);
51
-        if (code == "" && item["code2"] != "") {
52
-            code = item["code2"];
53
-        } else if (code == "") {
54
-            code = "---";
55
-        }
56
-        var price = item['price'];
57
-        if (price == null || price == "" || price == 0) {
58
-            if (!$(".list-group-item[data-code='" + code + "']").length) {
59
-                bsprompt("Enter Price",
60
-                        "No price set.  Enter a price for this item:",
61
-                        "Add Item",
62
-                        "Cancel",
63
-                        "number",
64
-                        function (result) {
65
-                            addItem(item['name'], code, result);
66
-                        });
67
-                return;
68
-            }
69
-        }
70
-        addItem(item['name'], code, price);
71
-    }
72
-    if (q == "") {
73
-        return;
74
-    }
75
-    $.get("action.php", {
76
-        action: "itemsearch",
77
-        q: q
78
-    }, function (data) {
79
-        if (data['items'].length == 1) {
80
-            decodeThenAddItem(data['items'][0]);
81
-        } else if (data['items'].length > 1) {
82
-            var options = [];
83
-            for (var i = 0; i < data['items'].length; i++) {
84
-                var text = data['items'][i]['name'];
85
-                if (data['items'][i]['price'] != null) {
86
-                    text += " <span class=\"ml-auto\">$" + data['items'][i]['price'] + "</span>";
87
-                }
88
-                options.push({"text": text, "val": data['items'][i]['id']});
89
-            }
90
-            bschoices(
91
-                    "Multiple Results",
92
-                    "More than one item match the query.  Pick the correct one:",
93
-                    options,
94
-                    "Cancel",
95
-                    function (result) {
96
-                        for (var i = 0; i < data['items'].length; i++) {
97
-                            if (data['items'][i]['id'] == result) {
98
-                                decodeThenAddItem(data['items'][i]);
99
-                                break;
100
-                            }
101
-                        }
102
-                    }
103
-            );
104
-        }
105
-    }).fail(function () {
106
-        alert("Error");
107
-    });
108
-}
109
-
110
-function removezero() {
111
-    $("#pos-lines-box .list-group-item").each(function () {
112
-        var qty = $(".item-qty", this).val() * 1.0;
113
-        if (qty == 0) {
114
-            $(this).remove();
115
-        }
116
-    });
117
-}
118
-
119 7
 function recalculate() {
120 8
     removezero();
121 9
     var total = 0.0;
10
+    var paid = 0.0;
11
+    var remaining = 0.0;
12
+    var change = 0.0;
122 13
     $("#pos-lines-box .list-group-item").each(function () {
123 14
         var each = $(".item-price", this).val() * 1.0;
124 15
         var qty = $(".item-qty", this).val() * 1.0;
@@ -128,56 +19,34 @@ function recalculate() {
128 19
         console.log(each.toFixed(2));
129 20
         total += line;
130 21
     });
131
-    $(".grand-total").text(total.toFixed(2));
132
-}
133 22
 
134
-function updateQty(btn, diff) {
135
-    var qtybox = $(btn).parent().parent().find(".item-qty");
136
-    var qty = parseInt(qtybox.val());
137
-    qty += diff;
138
-    if (qty > 0) {
139
-        qtybox.val(qty);
140
-        var minbtn = $(btn).parent().parent().find(".qty-minus");
141
-        if (qty == 1) {
142
-            minbtn.html("<i class=\"fas fa-trash\"></i>");
143
-        } else {
144
-            minbtn.html("<i class=\"fas fa-minus\"></i>");
145
-        }
23
+    $("#payment-lines .list-group-item").each(function () {
24
+        var line = $(".payment-entry", this).val() * 1.0;
25
+        paid += line;
26
+    });
27
+
28
+    remaining = total - paid;
29
+    change = (total - paid) * -1.0;
30
+    if (remaining <= 0) {
31
+        remaining = 0.0;
32
+        $("#owed-amount").parent().removeClass("font-weight-bold");
146 33
     } else {
147
-        qtybox.closest(".list-group-item").remove();
34
+        $("#owed-amount").parent().addClass("font-weight-bold");
148 35
     }
149
-    recalculate();
150
-}
151
-
152
-$("#pos-lines-box").on("click", ".qty-minus", function () {
153
-    updateQty(this, -1);
154
-});
155
-
156
-$("#pos-lines-box").on("click", ".qty-plus", function () {
157
-    updateQty(this, 1);
158
-});
159
-
160
-$("#pos-lines-box").on("change", ".item-qty,.item-price", function () {
161
-    recalculate();
162
-});
163 36
 
164
-$("#pos-lines-box").on("keypress", ".item-qty,.item-price", function (e) {
165
-    if (e.which === 13) {
166
-        recalculate();
37
+    if (change <= 0) {
38
+        change = 0.0;
39
+        $("#change-amount").parent().removeClass("font-weight-bold");
40
+    } else {
41
+        $("#change-amount").parent().addClass("font-weight-bold");
167 42
     }
168
-});
169 43
 
170
-$("#barcode").on('keypress', function (e) {
171
-    if (e.which === 13) {
172
-        findItem($("#barcode").val());
173
-        $("#barcode").val("");
174
-    }
175
-});
44
+    $("#grand-total").text(total.toFixed(2));
45
+    $("#paid-amount").text(paid.toFixed(2));
46
+    $("#owed-amount").text(remaining.toFixed(2));
47
+    $("#change-amount").text(change.toFixed(2));
48
+}
176 49
 
177
-$("#barcodebtn").on("click", function () {
178
-    findItem($("#barcode").val());
179
-    $("#barcode").val("");
180
-});
181 50
 
182 51
 $("body").on("keypress", "input[type=money],input[type=number]", function (e) {
183 52
     //console.log(String.fromCharCode(e.which) + "|" + e.which);
@@ -193,4 +62,7 @@ $("body").on("keypress", "input[type=money],input[type=number]", function (e) {
193 62
     }
194 63
 });
195 64
 
196
-$("#barcode").focus();
65
+// Make sure the session doesn't expire
66
+setInterval(function () {
67
+    $.get("action.php", {action: "session_keepalive"});
68
+}, 1000 * 60);

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

@@ -0,0 +1,82 @@
1
+/*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ */
6
+
7
+function sendTransactionToServer(callback) {
8
+    var items = [];
9
+    var payments = [];
10
+    var customer = '';
11
+    var register = '';
12
+    $("#pos-lines-box .list-group-item").each(function () {
13
+        var each = $(".item-price", this).val() * 1.0;
14
+        var qty = $(".item-qty", this).val() * 1.0;
15
+        var code = $(this).data("code");
16
+        var id = $(this).data("itemid");
17
+        items.push({
18
+            each: each,
19
+            qty: qty,
20
+            code: code,
21
+            id: id
22
+        });
23
+    });
24
+    $("#payment-lines .list-group-item").each(function () {
25
+        var amount = $(".payment-entry", this).val() * 1.0;
26
+        var type = $(".payment-entry", this).data("type");
27
+        var code = '';
28
+        if ($(".giftcard-number", this).length) {
29
+            code = $(".giftcard-number", this).val();
30
+        }
31
+        payments.push({
32
+            amount: amount,
33
+            type: type,
34
+            code: code
35
+        });
36
+    });
37
+
38
+    $.post("action.php", {
39
+        action: "finish_transaction",
40
+        items: items,
41
+        payments: payments,
42
+        customer: customer,
43
+        register: register
44
+    }, function (data) {
45
+        if (data.status == "OK") {
46
+            callback(data);
47
+        } else {
48
+            bsalert("Error", "The transaction could not be completed:<br />" + data.message);
49
+        }
50
+    }).fail(function () {
51
+        bsalert("Error", "The transaction could not be completed due to an unknown error.");
52
+    });
53
+}
54
+
55
+function showReceipt(txid) {
56
+    $("#receiptframe").attr("src", 'action.php?action=getreceipt&txid=' + txid);
57
+    $("#receiptmodal").modal();
58
+}
59
+
60
+function finishTransaction() {
61
+    sendTransactionToServer(function (data) {
62
+        showReceipt(data.txid);
63
+    });
64
+}
65
+
66
+$("#finishbtn").click(function () {
67
+    recalculate();
68
+    var owed = $("#owed-amount").text() * 1.0;
69
+    if (owed > 0) {
70
+        bsalert("Incomplete Transaction", "The customer still owes $" + owed.toFixed(2) + ".  Add a payment or remove items until everything is paid for.");
71
+    } else {
72
+        finishTransaction();
73
+    }
74
+});
75
+
76
+$("#receiptprintbtn").click(function () {
77
+    document.getElementById("receiptframe").contentWindow.print();
78
+})
79
+
80
+$("#receiptmodal").on("hide.bs.modal", function () {
81
+    window.location.reload();
82
+});

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

@@ -0,0 +1,167 @@
1
+/*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ */
6
+
7
+function addItem(name, code, price, id) {
8
+    if ($(".list-group-item[data-code='" + code + "']").length) {
9
+        updateQty($(".list-group-item[data-code='" + code + "']").find(".qty-plus"), 1);
10
+        return;
11
+    }
12
+    price = (price * 1.0).toFixed(2);
13
+    $("#pos-lines-box").append('<div class="list-group-item" data-code="' + code + '" data-itemid="' + id + '">'
14
+            + '<div class="d-flex w-100 justify-content-between mb-2">'
15
+            + '<h5 class="item-name">'
16
+            + name
17
+            + '</h5>'
18
+            + '<h5>'
19
+            + '<small class="item-code mr-1">' + code + '</small>'
20
+            + '<span class="badge badge-light">'
21
+            + '$<span class="line-total">'
22
+            + price
23
+            + '</span>'
24
+            + '</span>'
25
+            + '</h5>'
26
+            + '</div>'
27
+            + '<div class="d-inline-flex">'
28
+            + '<div class="input-group qty-control">'
29
+            + '<div class="input-group-prepend">'
30
+            + '<span class="input-group-text pr-1"><b>$</b></span>'
31
+            + '</div>'
32
+            + '<input type="money" class="form-control item-price" value="' + price + '"/>'
33
+            + '<div class="input-group-prepend">'
34
+            + '<span class="input-group-text px-2"><i class="fas fa-times"></i></span>'
35
+            + '<button class="btn btn-red qty-minus" type="button"><i class="fas fa-trash"></i></button>'
36
+            + '</div>'
37
+            + '<input type="number" class="form-control item-qty px-2" value="1" />'
38
+            + '<div class="input-group-append">'
39
+            + '<button class="btn btn-light-green qty-plus" type="button"><i class="fas fa-plus"></i></button>'
40
+            + '</div>'
41
+            + '</div>'
42
+            + '</div>'
43
+            + '</div>');
44
+    recalculate();
45
+}
46
+
47
+function findItem(q) {
48
+    function decodeThenAddItem(item) {
49
+        var code = item.code1;
50
+        console.log(code);
51
+        if (code == "" && item["code2"] != "") {
52
+            code = item["code2"];
53
+        } else if (code == "") {
54
+            code = "---";
55
+        }
56
+        var price = item['price'];
57
+        if (price == null || price == "" || price == 0) {
58
+            if (!$(".list-group-item[data-code='" + code + "']").length) {
59
+                bsprompt("Enter Price",
60
+                        "No price set.  Enter a price for this item:",
61
+                        "Add Item",
62
+                        "Cancel",
63
+                        "number",
64
+                        function (result) {
65
+                            addItem(item['name'], code, result, item['id']);
66
+                        });
67
+                return;
68
+            }
69
+        }
70
+        addItem(item['name'], code, price, item['id']);
71
+    }
72
+    if (q == "") {
73
+        return;
74
+    }
75
+    $.get("action.php", {
76
+        action: "itemsearch",
77
+        q: q
78
+    }, function (data) {
79
+        if (data['items'].length == 1) {
80
+            decodeThenAddItem(data['items'][0]);
81
+        } else if (data['items'].length > 1) {
82
+            var options = [];
83
+            for (var i = 0; i < data['items'].length; i++) {
84
+                var text = data['items'][i]['name'];
85
+                if (data['items'][i]['price'] != null) {
86
+                    text += " <span class=\"ml-auto\">$" + data['items'][i]['price'] + "</span>";
87
+                }
88
+                options.push({"text": text, "val": data['items'][i]['id']});
89
+            }
90
+            bschoices(
91
+                    "Multiple Results",
92
+                    "More than one item match the query.  Pick the correct one:",
93
+                    options,
94
+                    "Cancel",
95
+                    function (result) {
96
+                        for (var i = 0; i < data['items'].length; i++) {
97
+                            if (data['items'][i]['id'] == result) {
98
+                                decodeThenAddItem(data['items'][i]);
99
+                                break;
100
+                            }
101
+                        }
102
+                    }
103
+            );
104
+        }
105
+    }).fail(function () {
106
+        alert("Error");
107
+    });
108
+}
109
+
110
+function removezero() {
111
+    $("#pos-lines-box .list-group-item").each(function () {
112
+        var qty = $(".item-qty", this).val() * 1.0;
113
+        if (qty == 0) {
114
+            $(this).remove();
115
+        }
116
+    });
117
+}
118
+
119
+function updateQty(btn, diff) {
120
+    var qtybox = $(btn).parent().parent().find(".item-qty");
121
+    var qty = parseInt(qtybox.val());
122
+    qty += diff;
123
+    if (qty > 0) {
124
+        qtybox.val(qty);
125
+        var minbtn = $(btn).parent().parent().find(".qty-minus");
126
+        if (qty == 1) {
127
+            minbtn.html("<i class=\"fas fa-trash\"></i>");
128
+        } else {
129
+            minbtn.html("<i class=\"fas fa-minus\"></i>");
130
+        }
131
+    } else {
132
+        qtybox.closest(".list-group-item").remove();
133
+    }
134
+    recalculate();
135
+}
136
+
137
+$("#pos-lines-box").on("click", ".qty-minus", function () {
138
+    updateQty(this, -1);
139
+});
140
+
141
+$("#pos-lines-box").on("click", ".qty-plus", function () {
142
+    updateQty(this, 1);
143
+});
144
+
145
+$("#pos-lines-box").on("change blur", ".item-qty,.item-price", function () {
146
+    recalculate();
147
+});
148
+
149
+$("#pos-lines-box").on("keypress", ".item-qty,.item-price", function (e) {
150
+    if (e.which === 13) {
151
+        recalculate();
152
+    }
153
+});
154
+
155
+$("#barcode").on('keypress', function (e) {
156
+    if (e.which === 13) {
157
+        findItem($("#barcode").val());
158
+        $("#barcode").val("");
159
+    }
160
+});
161
+
162
+$("#barcodebtn").on("click", function () {
163
+    findItem($("#barcode").val());
164
+    $("#barcode").val("");
165
+});
166
+
167
+$("#barcode").focus();

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

@@ -0,0 +1,111 @@
1
+/*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ */
6
+
7
+function addPayment(type, icon, text) {
8
+    var extrafield = "";
9
+    if (type == "giftcard") {
10
+        extrafield = ''
11
+                + '        <div class="input-group-prepend input-group-append">'
12
+                + '            <span class="input-group-text">'
13
+                + '                #'
14
+                + '            </span>'
15
+                + '        </div>'
16
+                + '        <input class="form-control giftcard-number" type="number" />';
17
+    }
18
+    var amount = "";
19
+    // Autofill the exact due amount for payment methods that are flexible like that
20
+    if (type == "check" || type == "card" || type == "crypto") {
21
+        if ($("#owed-amount").text() * 1.0 > 0) {
22
+            amount = ($("#owed-amount").text() * 1.0).toFixed(2);
23
+        }
24
+    }
25
+    $("#payment-lines").append(''
26
+            + '<div class="list-group-item">'
27
+            + '    <div class="input-group">'
28
+            + '        <div class="input-group-prepend">'
29
+            + '            <span class="input-group-text">'
30
+            + '                <i class="' + icon + ' fa-fw mr-1"></i> '
31
+            + text
32
+            + '            </span>'
33
+            + '        </div>'
34
+            + '        <div class="input-group-prepend">'
35
+            + '            <span class="input-group-text">'
36
+            + '                $'
37
+            + '            </span>'
38
+            + '        </div>'
39
+            + '        <input class="form-control payment-entry" type="money" data-type="' + type + '" value="' + amount + '" />'
40
+            + extrafield
41
+            + '        <div class="input-group-append">'
42
+            + '            <span class="btn btn-outline-danger remove-payment-btn">'
43
+            + '                <i class="fas fa-trash"></i>'
44
+            + '            </span>'
45
+            + '        </div>'
46
+            + '    </div>'
47
+            + '</div>');
48
+    $("#payment-lines .payment-entry").last().focus();
49
+}
50
+
51
+function checkGiftCardBalance() {
52
+    $("#payment-lines .list-group-item:has(.giftcard-number)").each(function () {
53
+        var paymentbox = $(".payment-entry", this);
54
+        var cardnumberbox = $(".giftcard-number", this);
55
+        var amount = paymentbox.val() * 1.0;
56
+        var cardnumber = cardnumberbox.val();
57
+        if (cardnumber == "") {
58
+            return;
59
+        }
60
+        $.get("action.php", {
61
+            action: "giftcard_lookup",
62
+            code: cardnumber
63
+        }, function (json) {
64
+            if (json.status == "OK") {
65
+                if (json.cards.length == 0) {
66
+                    bsalert("Invalid Gift Card", "Gift card #" + cardnumber + " does not exist.  Remove it to finish the transaction.");
67
+                    cardnumberbox.addClass('is-invalid');
68
+                } else if (json.cards.length == 1) {
69
+                    if (json.cards[0].balance < amount) {
70
+                        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 + ").");
71
+                        paymentbox.val(json.cards[0].balance);
72
+                    }
73
+                    cardnumberbox.removeClass('is-invalid');
74
+                } else {
75
+                    bsalert("Invalid Gift Card", "Unable to determine which gift card #" + cardnumber + " is referring to.  Remove it to finish the transaction.");
76
+                    cardnumberbox.addClass('is-invalid');
77
+                }
78
+            } else {
79
+                bsalert("Invalid Gift Card", "Unable to determine which gift card #" + cardnumber + " is referring to.  Remove it to finish the transaction.");
80
+                cardnumberbox.addClass('is-invalid');
81
+            }
82
+        });
83
+    });
84
+}
85
+
86
+$("#payment-lines").on("change keyup blur", ".payment-entry", function () {
87
+    recalculate();
88
+});
89
+
90
+$("#payment-lines").on("blur", ".giftcard-number,.payment-entry[data-type=giftcard]", function () {
91
+    checkGiftCardBalance();
92
+});
93
+
94
+$("#payment-lines").on("keypress", ".giftcard-number,.payment-entry[data-type=giftcard]", function (e) {
95
+    if (e.which === 13) {
96
+        checkGiftCardBalance();
97
+    }
98
+});
99
+
100
+$("#payment-lines").on("click", ".remove-payment-btn", function () {
101
+    $(this).closest(".list-group-item").remove();
102
+    recalculate();
103
+});
104
+
105
+$("#paymentbtn").click(function () {
106
+    $("#paymentui").removeClass("d-none");
107
+});
108
+
109
+$(".payment-method-button").click(function () {
110
+    addPayment($(this).data("payment-method"), $(this).data("icon"), $(this).data("text"));
111
+});

Loading…
Cancel
Save