mirror of
https://github.com/myronblair/tomsjavajive-app
synced 2026-06-30 17:50:56 -05:00
1387 lines
44 KiB
PHP
1387 lines
44 KiB
PHP
<?php
|
|
/**
|
|
* Tom's Java Jive - Admin POS (Point of Sale) - Enhanced
|
|
*/
|
|
|
|
$pageTitle = 'Point of Sale';
|
|
require_once __DIR__ . '/includes/header.php';
|
|
|
|
// Get all active products
|
|
$products = db()->fetchAll(
|
|
"SELECT product_id, name, price, sale_price, stock, images, category, barcode, sku
|
|
FROM products WHERE is_active = 1 ORDER BY category, name"
|
|
);
|
|
|
|
// Group products by category
|
|
$productsByCategory = [];
|
|
foreach ($products as $product) {
|
|
$cat = $product['category'] ?? 'Other';
|
|
if (!isset($productsByCategory[$cat])) {
|
|
$productsByCategory[$cat] = [];
|
|
}
|
|
$product['images'] = json_decode($product['images'] ?? '[]', true);
|
|
$product['display_price'] = $product['sale_price'] ?? $product['price'];
|
|
$productsByCategory[$cat][] = $product;
|
|
}
|
|
|
|
// Get held orders
|
|
$heldOrders = $_SESSION['pos_held_orders'] ?? [];
|
|
|
|
// Get tax rate from settings
|
|
$taxRate = getSetting('tax_rate', 0) / 100;
|
|
?>
|
|
|
|
<style>
|
|
.pos-layout {
|
|
display: grid;
|
|
grid-template-columns: 1fr 400px;
|
|
gap: 1rem;
|
|
height: calc(100vh - 120px);
|
|
}
|
|
|
|
.pos-products {
|
|
background: var(--admin-surface);
|
|
border-radius: var(--admin-radius);
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.pos-search {
|
|
padding: 1rem;
|
|
border-bottom: 1px solid var(--admin-border);
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.pos-search input {
|
|
flex: 1;
|
|
padding: 0.75rem 1rem;
|
|
border: 1px solid var(--admin-border);
|
|
border-radius: var(--admin-radius);
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.pos-search .btn {
|
|
padding: 0.75rem;
|
|
}
|
|
|
|
.pos-categories {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
padding: 1rem;
|
|
border-bottom: 1px solid var(--admin-border);
|
|
overflow-x: auto;
|
|
flex-wrap: nowrap;
|
|
}
|
|
|
|
.pos-category-btn {
|
|
padding: 0.5rem 1rem;
|
|
border: 1px solid var(--admin-border);
|
|
border-radius: 20px;
|
|
background: var(--admin-surface);
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
font-size: 0.875rem;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.pos-category-btn:hover {
|
|
background: var(--admin-bg);
|
|
}
|
|
|
|
.pos-category-btn.active {
|
|
background: var(--admin-primary);
|
|
border-color: var(--admin-primary);
|
|
color: white;
|
|
}
|
|
|
|
.pos-product-grid {
|
|
flex: 1;
|
|
padding: 1rem;
|
|
overflow-y: auto;
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
|
gap: 0.75rem;
|
|
align-content: start;
|
|
}
|
|
|
|
.pos-product-item {
|
|
background: var(--admin-bg);
|
|
border-radius: var(--admin-radius);
|
|
padding: 0.75rem;
|
|
cursor: pointer;
|
|
text-align: center;
|
|
transition: all 0.2s;
|
|
border: 2px solid transparent;
|
|
}
|
|
|
|
.pos-product-item:hover {
|
|
border-color: var(--admin-primary);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.pos-product-item.out-of-stock {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.pos-product-item img {
|
|
width: 70px;
|
|
height: 70px;
|
|
object-fit: cover;
|
|
border-radius: var(--admin-radius);
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.pos-product-item .name {
|
|
font-size: 0.8rem;
|
|
font-weight: 500;
|
|
margin-bottom: 0.25rem;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
line-height: 1.2;
|
|
min-height: 2.4em;
|
|
}
|
|
|
|
.pos-product-item .price {
|
|
color: var(--admin-primary);
|
|
font-weight: 700;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.pos-product-item .stock {
|
|
font-size: 0.7rem;
|
|
color: var(--admin-text-muted);
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.pos-product-item .stock.low {
|
|
color: var(--admin-warning);
|
|
}
|
|
|
|
.pos-product-item .stock.out {
|
|
color: var(--admin-error);
|
|
}
|
|
|
|
/* Cart Section */
|
|
.pos-cart {
|
|
background: var(--admin-surface);
|
|
border-radius: var(--admin-radius);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.pos-cart-header {
|
|
padding: 1rem;
|
|
border-bottom: 1px solid var(--admin-border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.pos-cart-header h3 {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.pos-customer-badge {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.25rem 0.75rem;
|
|
background: var(--admin-bg);
|
|
border-radius: 20px;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.pos-customer-badge .remove {
|
|
cursor: pointer;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.pos-customer-badge .remove:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
.pos-cart-items {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 0.5rem 1rem;
|
|
min-height: 150px;
|
|
}
|
|
|
|
.pos-cart-item {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem 0;
|
|
border-bottom: 1px solid var(--admin-border);
|
|
align-items: center;
|
|
}
|
|
|
|
.pos-cart-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.pos-cart-item-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.pos-cart-item-name {
|
|
font-weight: 500;
|
|
font-size: 0.875rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.pos-cart-item-price {
|
|
color: var(--admin-text-muted);
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.pos-cart-item-qty {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.pos-cart-item-qty button {
|
|
width: 26px;
|
|
height: 26px;
|
|
border: 1px solid var(--admin-border);
|
|
border-radius: 4px;
|
|
background: var(--admin-surface);
|
|
cursor: pointer;
|
|
font-size: 0.875rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.pos-cart-item-qty button:hover {
|
|
background: var(--admin-bg);
|
|
}
|
|
|
|
.pos-cart-item-qty span {
|
|
width: 30px;
|
|
text-align: center;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.pos-cart-item-total {
|
|
font-weight: 600;
|
|
min-width: 60px;
|
|
text-align: right;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.pos-cart-item-remove {
|
|
color: var(--admin-error);
|
|
cursor: pointer;
|
|
opacity: 0.5;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.pos-cart-item-remove:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Discount Section */
|
|
.pos-discount-section {
|
|
padding: 0.75rem 1rem;
|
|
border-top: 1px solid var(--admin-border);
|
|
background: var(--admin-bg);
|
|
}
|
|
|
|
.pos-discount-row {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.pos-discount-row input {
|
|
flex: 1;
|
|
padding: 0.5rem;
|
|
border: 1px solid var(--admin-border);
|
|
border-radius: var(--admin-radius);
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.pos-applied-discount {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.5rem;
|
|
background: rgba(16, 185, 129, 0.1);
|
|
border-radius: var(--admin-radius);
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.pos-applied-discount .remove {
|
|
cursor: pointer;
|
|
color: var(--admin-error);
|
|
}
|
|
|
|
/* Totals */
|
|
.pos-cart-totals {
|
|
padding: 1rem;
|
|
border-top: 1px solid var(--admin-border);
|
|
background: var(--admin-bg);
|
|
}
|
|
|
|
.pos-cart-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 0.5rem;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.pos-cart-row.discount {
|
|
color: var(--admin-success);
|
|
}
|
|
|
|
.pos-cart-row.total {
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
border-top: 2px solid var(--admin-border);
|
|
padding-top: 0.75rem;
|
|
margin-top: 0.5rem;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.pos-cart-actions {
|
|
padding: 1rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.pos-cart-actions .btn-primary {
|
|
padding: 1rem;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.pos-empty {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
color: var(--admin-text-muted);
|
|
}
|
|
|
|
.pos-empty i {
|
|
font-size: 2.5rem;
|
|
margin-bottom: 0.75rem;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
/* Held Orders Badge */
|
|
.held-orders-badge {
|
|
position: relative;
|
|
}
|
|
|
|
.held-orders-badge .count {
|
|
position: absolute;
|
|
top: -5px;
|
|
right: -5px;
|
|
background: var(--admin-error);
|
|
color: white;
|
|
font-size: 0.7rem;
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
/* Receipt Modal */
|
|
.receipt {
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 12px;
|
|
max-width: 280px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: white;
|
|
color: black;
|
|
}
|
|
|
|
.receipt-header {
|
|
text-align: center;
|
|
margin-bottom: 15px;
|
|
border-bottom: 1px dashed #000;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.receipt-header h2 {
|
|
margin: 0 0 5px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.receipt-items {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.receipt-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.receipt-totals {
|
|
border-top: 1px dashed #000;
|
|
padding-top: 10px;
|
|
}
|
|
|
|
.receipt-total-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 3px;
|
|
}
|
|
|
|
.receipt-total-row.grand-total {
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
border-top: 1px solid #000;
|
|
padding-top: 5px;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.receipt-footer {
|
|
text-align: center;
|
|
margin-top: 15px;
|
|
border-top: 1px dashed #000;
|
|
padding-top: 10px;
|
|
font-size: 11px;
|
|
}
|
|
|
|
@media print {
|
|
body * {
|
|
visibility: hidden;
|
|
}
|
|
#receiptContent, #receiptContent * {
|
|
visibility: visible;
|
|
}
|
|
#receiptContent {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div class="pos-layout">
|
|
<!-- Products Section -->
|
|
<div class="pos-products">
|
|
<div class="pos-search">
|
|
<input type="text" id="pos-search" placeholder="Search products or scan barcode..." autofocus>
|
|
<button class="btn btn-secondary" onclick="document.getElementById('pos-search').value=''; filterProducts();" title="Clear">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="pos-categories">
|
|
<button class="pos-category-btn active" data-category="all">All</button>
|
|
<?php foreach (array_keys($productsByCategory) as $category): ?>
|
|
<button class="pos-category-btn" data-category="<?= htmlspecialchars($category) ?>">
|
|
<?= htmlspecialchars(ucfirst($category)) ?>
|
|
</button>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
|
|
<div class="pos-product-grid" id="product-grid">
|
|
<?php if (empty($products)): ?>
|
|
<div style="grid-column: 1 / -1; text-align: center; padding: 3rem; color: var(--admin-text-muted);">
|
|
<i class="fas fa-box-open" style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.5;"></i>
|
|
<p>No products available</p>
|
|
<a href="/admin/product-edit.php" class="btn btn-primary mt-1">Add Products</a>
|
|
</div>
|
|
<?php else: ?>
|
|
<?php foreach ($products as $product):
|
|
$images = json_decode($product['images'] ?? '[]', true);
|
|
$imageUrl = !empty($images) ? $images[0] : '/assets/images/placeholder-product.svg';
|
|
$isOutOfStock = $product['stock'] <= 0;
|
|
$isLowStock = $product['stock'] > 0 && $product['stock'] <= 5;
|
|
?>
|
|
<div class="pos-product-item <?= $isOutOfStock ? 'out-of-stock' : '' ?>"
|
|
data-id="<?= $product['product_id'] ?>"
|
|
data-name="<?= htmlspecialchars($product['name']) ?>"
|
|
data-price="<?= $product['sale_price'] ?? $product['price'] ?>"
|
|
data-stock="<?= $product['stock'] ?>"
|
|
data-category="<?= htmlspecialchars($product['category'] ?? 'Other') ?>"
|
|
data-barcode="<?= htmlspecialchars($product['barcode'] ?? '') ?>"
|
|
data-sku="<?= htmlspecialchars($product['sku'] ?? '') ?>">
|
|
<img src="<?= htmlspecialchars($imageUrl) ?>" alt="" onerror="this.src='/assets/images/placeholder-product.svg'">
|
|
<div class="name"><?= htmlspecialchars($product['name']) ?></div>
|
|
<div class="price"><?= formatCurrency($product['sale_price'] ?? $product['price']) ?></div>
|
|
<div class="stock <?= $isOutOfStock ? 'out' : ($isLowStock ? 'low' : '') ?>">
|
|
<?= $isOutOfStock ? 'Out of stock' : $product['stock'] . ' in stock' ?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cart Section -->
|
|
<div class="pos-cart">
|
|
<div class="pos-cart-header">
|
|
<h3><i class="fas fa-shopping-cart"></i> Current Sale</h3>
|
|
<div id="customer-display">
|
|
<button class="btn btn-sm btn-secondary" onclick="openCustomerModal()">
|
|
<i class="fas fa-user-plus"></i> Customer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pos-cart-items" id="cart-items">
|
|
<div class="pos-empty" id="cart-empty">
|
|
<i class="fas fa-shopping-basket"></i>
|
|
<p>Add products to start a sale</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Discount Section -->
|
|
<div class="pos-discount-section" id="discount-section">
|
|
<div class="pos-discount-row">
|
|
<input type="text" id="coupon-input" placeholder="Coupon code" style="text-transform: uppercase;">
|
|
<button class="btn btn-sm btn-secondary" onclick="applyCoupon()">Apply</button>
|
|
</div>
|
|
<div class="pos-discount-row">
|
|
<input type="number" id="manual-discount" placeholder="Manual discount $" step="0.01" min="0">
|
|
<button class="btn btn-sm btn-secondary" onclick="applyManualDiscount()">Apply</button>
|
|
</div>
|
|
<div id="applied-discounts"></div>
|
|
</div>
|
|
|
|
<div class="pos-cart-totals">
|
|
<div class="pos-cart-row">
|
|
<span>Subtotal</span>
|
|
<span id="cart-subtotal">$0.00</span>
|
|
</div>
|
|
<div class="pos-cart-row discount" id="discount-row" style="display: none;">
|
|
<span>Discount</span>
|
|
<span id="cart-discount">-$0.00</span>
|
|
</div>
|
|
<div class="pos-cart-row">
|
|
<span>Tax (<?= ($taxRate * 100) ?>%)</span>
|
|
<span id="cart-tax">$0.00</span>
|
|
</div>
|
|
<div class="pos-cart-row total">
|
|
<span>Total</span>
|
|
<span id="cart-total">$0.00</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pos-cart-actions">
|
|
<button class="btn btn-primary" id="btn-checkout" disabled>
|
|
<i class="fas fa-credit-card"></i> Pay Now
|
|
</button>
|
|
<div style="display: flex; gap: 0.5rem;">
|
|
<button class="btn btn-secondary held-orders-badge" id="btn-held" style="flex: 1;" onclick="openHeldOrdersModal()">
|
|
<i class="fas fa-pause"></i> Held
|
|
<span class="count" id="held-count" style="display: none;">0</span>
|
|
</button>
|
|
<button class="btn btn-secondary" id="btn-hold" style="flex: 1;" disabled onclick="holdOrder()">
|
|
<i class="fas fa-clock"></i> Hold
|
|
</button>
|
|
<button class="btn btn-danger" id="btn-clear" style="flex: 1;" disabled onclick="clearCart()">
|
|
<i class="fas fa-trash"></i> Clear
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Payment Modal -->
|
|
<div class="modal-overlay" id="paymentModal">
|
|
<div class="modal" style="max-width: 450px;">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">Process Payment</h3>
|
|
<button type="button" class="modal-close" onclick="Modal.close('paymentModal')">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div style="text-align: center; margin-bottom: 1.5rem; padding: 1rem; background: var(--admin-bg); border-radius: var(--admin-radius);">
|
|
<div style="font-size: 0.9rem; color: var(--admin-text-muted);">Amount Due</div>
|
|
<div style="font-size: 2rem; font-weight: 700; color: var(--admin-primary);" id="payment-amount">$0.00</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Payment Method</label>
|
|
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem;">
|
|
<button type="button" class="btn btn-secondary payment-method-btn active" data-method="cash">
|
|
<i class="fas fa-money-bill-wave"></i><br>Cash
|
|
</button>
|
|
<button type="button" class="btn btn-secondary payment-method-btn" data-method="card">
|
|
<i class="fas fa-credit-card"></i><br>Card
|
|
</button>
|
|
<button type="button" class="btn btn-secondary payment-method-btn" data-method="wallet" id="wallet-btn" style="display: none;">
|
|
<i class="fas fa-wallet"></i><br>Wallet
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="cash-fields">
|
|
<div class="form-group">
|
|
<label class="form-label">Amount Received</label>
|
|
<input type="number" id="cash-received" class="form-input" step="0.01" min="0" style="font-size: 1.25rem; text-align: center;">
|
|
</div>
|
|
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.5rem; margin-bottom: 1rem;">
|
|
<button type="button" class="btn btn-secondary quick-cash" data-amount="5">$5</button>
|
|
<button type="button" class="btn btn-secondary quick-cash" data-amount="10">$10</button>
|
|
<button type="button" class="btn btn-secondary quick-cash" data-amount="20">$20</button>
|
|
<button type="button" class="btn btn-secondary quick-cash" data-amount="50">$50</button>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Change Due</label>
|
|
<input type="text" id="change-due" class="form-input" readonly value="$0.00" style="font-size: 1.25rem; text-align: center; font-weight: 700; color: var(--admin-success);">
|
|
</div>
|
|
</div>
|
|
|
|
<div id="wallet-info" style="display: none; padding: 1rem; background: var(--admin-bg); border-radius: var(--admin-radius); margin-bottom: 1rem;">
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span>Wallet Balance:</span>
|
|
<strong id="wallet-balance">$0.00</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Order Notes</label>
|
|
<input type="text" id="order-notes" class="form-input" placeholder="Any special notes...">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" onclick="Modal.close('paymentModal')">Cancel</button>
|
|
<button type="button" class="btn btn-primary btn-lg" id="btn-complete-payment" style="min-width: 150px;">
|
|
<i class="fas fa-check"></i> Complete Sale
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Customer Search Modal -->
|
|
<div class="modal-overlay" id="customerModal">
|
|
<div class="modal" style="max-width: 500px;">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">Select Customer</h3>
|
|
<button type="button" class="modal-close" onclick="Modal.close('customerModal')">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-group">
|
|
<input type="text" id="customer-search-input" class="form-input" placeholder="Search by name, email, or phone..." oninput="searchCustomers(this.value)">
|
|
</div>
|
|
<div id="customer-results" style="max-height: 300px; overflow-y: auto;">
|
|
<p class="text-muted text-center">Type to search customers</p>
|
|
</div>
|
|
<hr>
|
|
<p class="text-center">
|
|
<button class="btn btn-secondary" onclick="Modal.close('customerModal')">Continue as Guest</button>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Held Orders Modal -->
|
|
<div class="modal-overlay" id="heldOrdersModal">
|
|
<div class="modal" style="max-width: 500px;">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">Held Orders</h3>
|
|
<button type="button" class="modal-close" onclick="Modal.close('heldOrdersModal')">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="held-orders-list">
|
|
<p class="text-muted text-center">No held orders</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Receipt Modal -->
|
|
<div class="modal-overlay" id="receiptModal">
|
|
<div class="modal" style="max-width: 350px;">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">Receipt</h3>
|
|
<button type="button" class="modal-close" onclick="Modal.close('receiptModal')">×</button>
|
|
</div>
|
|
<div class="modal-body" style="padding: 0;">
|
|
<div id="receiptContent"></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" onclick="Modal.close('receiptModal')">Close</button>
|
|
<button type="button" class="btn btn-primary" onclick="printReceipt()">
|
|
<i class="fas fa-print"></i> Print
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// POS State
|
|
const POS = {
|
|
cart: [],
|
|
customer: null,
|
|
coupon: null,
|
|
manualDiscount: 0,
|
|
taxRate: <?= $taxRate ?>,
|
|
heldOrders: JSON.parse(localStorage.getItem('pos_held_orders') || '[]'),
|
|
paymentMethod: 'cash'
|
|
};
|
|
|
|
// DOM Elements
|
|
const cartItemsEl = document.getElementById('cart-items');
|
|
const cartEmptyEl = document.getElementById('cart-empty');
|
|
const searchEl = document.getElementById('pos-search');
|
|
|
|
// Add product to cart
|
|
function addToCart(product) {
|
|
if (product.stock <= 0) {
|
|
AdminToast.error('Product is out of stock');
|
|
return;
|
|
}
|
|
|
|
const existing = POS.cart.find(item => item.id === product.id);
|
|
|
|
if (existing) {
|
|
if (existing.quantity < product.stock) {
|
|
existing.quantity++;
|
|
} else {
|
|
AdminToast.error('Maximum stock reached');
|
|
return;
|
|
}
|
|
} else {
|
|
POS.cart.push({
|
|
id: product.id,
|
|
name: product.name,
|
|
price: parseFloat(product.price),
|
|
quantity: 1,
|
|
stock: parseInt(product.stock)
|
|
});
|
|
}
|
|
|
|
renderCart();
|
|
}
|
|
|
|
// Update quantity
|
|
function updateQuantity(id, delta) {
|
|
const item = POS.cart.find(i => i.id === id);
|
|
if (!item) return;
|
|
|
|
const newQty = item.quantity + delta;
|
|
if (newQty <= 0) {
|
|
removeFromCart(id);
|
|
} else if (newQty <= item.stock) {
|
|
item.quantity = newQty;
|
|
renderCart();
|
|
} else {
|
|
AdminToast.error('Maximum stock reached');
|
|
}
|
|
}
|
|
|
|
// Remove from cart
|
|
function removeFromCart(id) {
|
|
POS.cart = POS.cart.filter(i => i.id !== id);
|
|
renderCart();
|
|
}
|
|
|
|
// Calculate totals
|
|
function calculateTotals() {
|
|
const subtotal = POS.cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
|
|
|
let discount = POS.manualDiscount;
|
|
if (POS.coupon) {
|
|
if (POS.coupon.type === 'percentage') {
|
|
discount += subtotal * (POS.coupon.value / 100);
|
|
} else {
|
|
discount += Math.min(POS.coupon.value, subtotal);
|
|
}
|
|
}
|
|
|
|
const taxable = Math.max(0, subtotal - discount);
|
|
const tax = taxable * POS.taxRate;
|
|
const total = taxable + tax;
|
|
|
|
return { subtotal, discount, tax, total };
|
|
}
|
|
|
|
// Render cart
|
|
function renderCart() {
|
|
const { subtotal, discount, tax, total } = calculateTotals();
|
|
|
|
if (POS.cart.length === 0) {
|
|
cartEmptyEl.style.display = 'block';
|
|
cartItemsEl.querySelectorAll('.pos-cart-item').forEach(el => el.remove());
|
|
} else {
|
|
cartEmptyEl.style.display = 'none';
|
|
|
|
let html = '';
|
|
POS.cart.forEach(item => {
|
|
html += `
|
|
<div class="pos-cart-item">
|
|
<div class="pos-cart-item-info">
|
|
<div class="pos-cart-item-name">${escapeHtml(item.name)}</div>
|
|
<div class="pos-cart-item-price">$${item.price.toFixed(2)} each</div>
|
|
</div>
|
|
<div class="pos-cart-item-qty">
|
|
<button onclick="updateQuantity('${item.id}', -1)">-</button>
|
|
<span>${item.quantity}</span>
|
|
<button onclick="updateQuantity('${item.id}', 1)">+</button>
|
|
</div>
|
|
<div class="pos-cart-item-total">$${(item.price * item.quantity).toFixed(2)}</div>
|
|
<i class="fas fa-times pos-cart-item-remove" onclick="removeFromCart('${item.id}')"></i>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
cartItemsEl.querySelectorAll('.pos-cart-item').forEach(el => el.remove());
|
|
cartItemsEl.insertAdjacentHTML('beforeend', html);
|
|
}
|
|
|
|
document.getElementById('cart-subtotal').textContent = '$' + subtotal.toFixed(2);
|
|
document.getElementById('cart-discount').textContent = '-$' + discount.toFixed(2);
|
|
document.getElementById('discount-row').style.display = discount > 0 ? 'flex' : 'none';
|
|
document.getElementById('cart-tax').textContent = '$' + tax.toFixed(2);
|
|
document.getElementById('cart-total').textContent = '$' + total.toFixed(2);
|
|
|
|
const hasItems = POS.cart.length > 0;
|
|
document.getElementById('btn-checkout').disabled = !hasItems;
|
|
document.getElementById('btn-hold').disabled = !hasItems;
|
|
document.getElementById('btn-clear').disabled = !hasItems;
|
|
|
|
updateHeldCount();
|
|
}
|
|
|
|
// Filter products
|
|
function filterProducts() {
|
|
const query = searchEl.value.toLowerCase();
|
|
const activeCategory = document.querySelector('.pos-category-btn.active')?.dataset.category || 'all';
|
|
|
|
document.querySelectorAll('.pos-product-item').forEach(item => {
|
|
const name = item.dataset.name.toLowerCase();
|
|
const barcode = (item.dataset.barcode || '').toLowerCase();
|
|
const sku = (item.dataset.sku || '').toLowerCase();
|
|
const category = item.dataset.category;
|
|
|
|
const matchesSearch = !query || name.includes(query) || barcode.includes(query) || sku.includes(query);
|
|
const matchesCategory = activeCategory === 'all' || category === activeCategory;
|
|
|
|
item.style.display = (matchesSearch && matchesCategory) ? '' : 'none';
|
|
});
|
|
}
|
|
|
|
// Product click handlers
|
|
document.querySelectorAll('.pos-product-item').forEach(el => {
|
|
el.addEventListener('click', function() {
|
|
if (this.classList.contains('out-of-stock')) return;
|
|
addToCart({
|
|
id: this.dataset.id,
|
|
name: this.dataset.name,
|
|
price: this.dataset.price,
|
|
stock: parseInt(this.dataset.stock)
|
|
});
|
|
});
|
|
});
|
|
|
|
// Category filter
|
|
document.querySelectorAll('.pos-category-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
document.querySelectorAll('.pos-category-btn').forEach(b => b.classList.remove('active'));
|
|
this.classList.add('active');
|
|
filterProducts();
|
|
});
|
|
});
|
|
|
|
// Search
|
|
searchEl.addEventListener('input', filterProducts);
|
|
|
|
// Barcode scanner - detect quick input
|
|
let barcodeBuffer = '';
|
|
let barcodeTimeout;
|
|
searchEl.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter') {
|
|
const barcode = this.value.trim();
|
|
if (barcode) {
|
|
const product = document.querySelector(`.pos-product-item[data-barcode="${barcode}"]`) ||
|
|
document.querySelector(`.pos-product-item[data-sku="${barcode}"]`);
|
|
if (product && !product.classList.contains('out-of-stock')) {
|
|
product.click();
|
|
this.value = '';
|
|
AdminToast.success('Product added');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Clear cart
|
|
function clearCart() {
|
|
if (POS.cart.length === 0) return;
|
|
if (confirm('Clear the entire cart?')) {
|
|
POS.cart = [];
|
|
POS.coupon = null;
|
|
POS.manualDiscount = 0;
|
|
document.getElementById('coupon-input').value = '';
|
|
document.getElementById('manual-discount').value = '';
|
|
document.getElementById('applied-discounts').innerHTML = '';
|
|
renderCart();
|
|
}
|
|
}
|
|
|
|
// Apply coupon
|
|
async function applyCoupon() {
|
|
const code = document.getElementById('coupon-input').value.trim().toUpperCase();
|
|
if (!code) return;
|
|
|
|
try {
|
|
const response = await fetch('/api/validate-coupon.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ code, subtotal: calculateTotals().subtotal })
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.error) {
|
|
AdminToast.error(data.error);
|
|
} else {
|
|
POS.coupon = {
|
|
code: code,
|
|
type: data.type,
|
|
value: data.value
|
|
};
|
|
renderAppliedDiscounts();
|
|
renderCart();
|
|
AdminToast.success('Coupon applied');
|
|
}
|
|
} catch (err) {
|
|
AdminToast.error('Failed to validate coupon');
|
|
}
|
|
}
|
|
|
|
// Apply manual discount
|
|
function applyManualDiscount() {
|
|
const amount = parseFloat(document.getElementById('manual-discount').value) || 0;
|
|
if (amount <= 0) return;
|
|
|
|
POS.manualDiscount = amount;
|
|
renderAppliedDiscounts();
|
|
renderCart();
|
|
}
|
|
|
|
// Render applied discounts
|
|
function renderAppliedDiscounts() {
|
|
let html = '';
|
|
|
|
if (POS.coupon) {
|
|
const display = POS.coupon.type === 'percentage'
|
|
? `${POS.coupon.value}% off`
|
|
: `$${POS.coupon.value.toFixed(2)} off`;
|
|
html += `
|
|
<div class="pos-applied-discount">
|
|
<span><i class="fas fa-tag"></i> ${POS.coupon.code}: ${display}</span>
|
|
<i class="fas fa-times remove" onclick="removeCoupon()"></i>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (POS.manualDiscount > 0) {
|
|
html += `
|
|
<div class="pos-applied-discount" style="margin-top: 0.5rem;">
|
|
<span><i class="fas fa-percent"></i> Manual: -$${POS.manualDiscount.toFixed(2)}</span>
|
|
<i class="fas fa-times remove" onclick="removeManualDiscount()"></i>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
document.getElementById('applied-discounts').innerHTML = html;
|
|
}
|
|
|
|
function removeCoupon() {
|
|
POS.coupon = null;
|
|
document.getElementById('coupon-input').value = '';
|
|
renderAppliedDiscounts();
|
|
renderCart();
|
|
}
|
|
|
|
function removeManualDiscount() {
|
|
POS.manualDiscount = 0;
|
|
document.getElementById('manual-discount').value = '';
|
|
renderAppliedDiscounts();
|
|
renderCart();
|
|
}
|
|
|
|
// Customer search
|
|
async function searchCustomers(query) {
|
|
if (query.length < 2) {
|
|
document.getElementById('customer-results').innerHTML = '<p class="text-muted text-center">Type to search customers</p>';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/search-customers.php?q=' + encodeURIComponent(query));
|
|
const data = await response.json();
|
|
|
|
if (data.length === 0) {
|
|
document.getElementById('customer-results').innerHTML = '<p class="text-muted text-center">No customers found</p>';
|
|
} else {
|
|
let html = '';
|
|
data.forEach(c => {
|
|
html += `
|
|
<div style="padding: 0.75rem; border-bottom: 1px solid var(--admin-border); cursor: pointer; display: flex; justify-content: space-between; align-items: center;"
|
|
onclick="selectCustomer(${JSON.stringify(c).replace(/"/g, '"')})">
|
|
<div>
|
|
<strong>${escapeHtml(c.name || c.email)}</strong><br>
|
|
<small class="text-muted">${escapeHtml(c.email)}${c.phone ? ' • ' + escapeHtml(c.phone) : ''}</small>
|
|
</div>
|
|
<div style="text-align: right;">
|
|
<div style="color: var(--admin-success);">$${parseFloat(c.wallet_balance || 0).toFixed(2)}</div>
|
|
<small class="text-muted">${c.reward_points || 0} pts</small>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
document.getElementById('customer-results').innerHTML = html;
|
|
}
|
|
} catch (err) {
|
|
document.getElementById('customer-results').innerHTML = '<p class="text-muted text-center">Search failed</p>';
|
|
}
|
|
}
|
|
|
|
function selectCustomer(customer) {
|
|
POS.customer = customer;
|
|
|
|
document.getElementById('customer-display').innerHTML = `
|
|
<div class="pos-customer-badge">
|
|
<i class="fas fa-user"></i>
|
|
<span>${escapeHtml(customer.name || customer.email)}</span>
|
|
<span class="remove" onclick="removeCustomer(); event.stopPropagation();">×</span>
|
|
</div>
|
|
`;
|
|
|
|
Modal.close('customerModal');
|
|
|
|
// Show wallet payment option
|
|
if (parseFloat(customer.wallet_balance) > 0) {
|
|
document.getElementById('wallet-btn').style.display = '';
|
|
}
|
|
}
|
|
|
|
function removeCustomer() {
|
|
POS.customer = null;
|
|
document.getElementById('customer-display').innerHTML = `
|
|
<button class="btn btn-sm btn-secondary" onclick="openCustomerModal()">
|
|
<i class="fas fa-user-plus"></i> Customer
|
|
</button>
|
|
`;
|
|
document.getElementById('wallet-btn').style.display = 'none';
|
|
}
|
|
|
|
function openCustomerModal() {
|
|
document.getElementById('customer-search-input').value = '';
|
|
document.getElementById('customer-results').innerHTML = '<p class="text-muted text-center">Type to search customers</p>';
|
|
Modal.open('customerModal');
|
|
setTimeout(() => document.getElementById('customer-search-input').focus(), 100);
|
|
}
|
|
|
|
// Hold order
|
|
function holdOrder() {
|
|
if (POS.cart.length === 0) return;
|
|
|
|
const heldOrder = {
|
|
id: Date.now(),
|
|
cart: [...POS.cart],
|
|
customer: POS.customer,
|
|
coupon: POS.coupon,
|
|
manualDiscount: POS.manualDiscount,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
POS.heldOrders.push(heldOrder);
|
|
localStorage.setItem('pos_held_orders', JSON.stringify(POS.heldOrders));
|
|
|
|
// Clear current
|
|
POS.cart = [];
|
|
POS.coupon = null;
|
|
POS.manualDiscount = 0;
|
|
removeCustomer();
|
|
document.getElementById('coupon-input').value = '';
|
|
document.getElementById('manual-discount').value = '';
|
|
document.getElementById('applied-discounts').innerHTML = '';
|
|
|
|
renderCart();
|
|
AdminToast.success('Order held');
|
|
}
|
|
|
|
// Recall held order
|
|
function recallHeldOrder(id) {
|
|
const index = POS.heldOrders.findIndex(o => o.id === id);
|
|
if (index === -1) return;
|
|
|
|
const order = POS.heldOrders[index];
|
|
|
|
// Check if current cart has items
|
|
if (POS.cart.length > 0) {
|
|
if (!confirm('This will replace your current cart. Continue?')) return;
|
|
}
|
|
|
|
POS.cart = order.cart;
|
|
POS.coupon = order.coupon;
|
|
POS.manualDiscount = order.manualDiscount;
|
|
if (order.customer) selectCustomer(order.customer);
|
|
|
|
// Remove from held
|
|
POS.heldOrders.splice(index, 1);
|
|
localStorage.setItem('pos_held_orders', JSON.stringify(POS.heldOrders));
|
|
|
|
renderAppliedDiscounts();
|
|
renderCart();
|
|
Modal.close('heldOrdersModal');
|
|
AdminToast.success('Order recalled');
|
|
}
|
|
|
|
function deleteHeldOrder(id) {
|
|
POS.heldOrders = POS.heldOrders.filter(o => o.id !== id);
|
|
localStorage.setItem('pos_held_orders', JSON.stringify(POS.heldOrders));
|
|
openHeldOrdersModal();
|
|
updateHeldCount();
|
|
}
|
|
|
|
function openHeldOrdersModal() {
|
|
if (POS.heldOrders.length === 0) {
|
|
document.getElementById('held-orders-list').innerHTML = '<p class="text-muted text-center">No held orders</p>';
|
|
} else {
|
|
let html = '';
|
|
POS.heldOrders.forEach(order => {
|
|
const total = order.cart.reduce((sum, i) => sum + (i.price * i.quantity), 0);
|
|
const time = new Date(order.timestamp).toLocaleTimeString();
|
|
html += `
|
|
<div style="padding: 1rem; border-bottom: 1px solid var(--admin-border); display: flex; justify-content: space-between; align-items: center;">
|
|
<div>
|
|
<strong>${order.customer ? escapeHtml(order.customer.name || order.customer.email) : 'Guest'}</strong><br>
|
|
<small class="text-muted">${order.cart.length} items • $${total.toFixed(2)} • ${time}</small>
|
|
</div>
|
|
<div style="display: flex; gap: 0.5rem;">
|
|
<button class="btn btn-sm btn-primary" onclick="recallHeldOrder(${order.id})">Recall</button>
|
|
<button class="btn btn-sm btn-danger" onclick="deleteHeldOrder(${order.id})"><i class="fas fa-trash"></i></button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
document.getElementById('held-orders-list').innerHTML = html;
|
|
}
|
|
Modal.open('heldOrdersModal');
|
|
}
|
|
|
|
function updateHeldCount() {
|
|
const count = POS.heldOrders.length;
|
|
const badge = document.getElementById('held-count');
|
|
if (count > 0) {
|
|
badge.textContent = count;
|
|
badge.style.display = 'flex';
|
|
} else {
|
|
badge.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Checkout
|
|
document.getElementById('btn-checkout').addEventListener('click', function() {
|
|
const { total } = calculateTotals();
|
|
document.getElementById('payment-amount').textContent = '$' + total.toFixed(2);
|
|
document.getElementById('cash-received').value = total.toFixed(2);
|
|
document.getElementById('change-due').value = '$0.00';
|
|
|
|
if (POS.customer) {
|
|
document.getElementById('wallet-balance').textContent = '$' + parseFloat(POS.customer.wallet_balance || 0).toFixed(2);
|
|
}
|
|
|
|
POS.paymentMethod = 'cash';
|
|
document.querySelectorAll('.payment-method-btn').forEach(b => b.classList.remove('active'));
|
|
document.querySelector('[data-method="cash"]').classList.add('active');
|
|
document.getElementById('cash-fields').style.display = 'block';
|
|
document.getElementById('wallet-info').style.display = 'none';
|
|
|
|
Modal.open('paymentModal');
|
|
setTimeout(() => document.getElementById('cash-received').focus(), 100);
|
|
});
|
|
|
|
// Payment method selection
|
|
document.querySelectorAll('.payment-method-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
document.querySelectorAll('.payment-method-btn').forEach(b => b.classList.remove('active'));
|
|
this.classList.add('active');
|
|
POS.paymentMethod = this.dataset.method;
|
|
|
|
document.getElementById('cash-fields').style.display = POS.paymentMethod === 'cash' ? 'block' : 'none';
|
|
document.getElementById('wallet-info').style.display = POS.paymentMethod === 'wallet' ? 'block' : 'none';
|
|
});
|
|
});
|
|
|
|
// Quick cash buttons
|
|
document.querySelectorAll('.quick-cash').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
document.getElementById('cash-received').value = this.dataset.amount;
|
|
calculateChange();
|
|
});
|
|
});
|
|
|
|
// Cash change calculation
|
|
document.getElementById('cash-received').addEventListener('input', calculateChange);
|
|
|
|
function calculateChange() {
|
|
const received = parseFloat(document.getElementById('cash-received').value) || 0;
|
|
const { total } = calculateTotals();
|
|
const change = received - total;
|
|
document.getElementById('change-due').value = '$' + Math.max(0, change).toFixed(2);
|
|
}
|
|
|
|
// Complete payment
|
|
document.getElementById('btn-complete-payment').addEventListener('click', async function() {
|
|
const { subtotal, discount, total } = calculateTotals();
|
|
|
|
if (POS.paymentMethod === 'cash') {
|
|
const received = parseFloat(document.getElementById('cash-received').value) || 0;
|
|
if (received < total) {
|
|
AdminToast.error('Insufficient payment amount');
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (POS.paymentMethod === 'wallet') {
|
|
if (!POS.customer || parseFloat(POS.customer.wallet_balance) < total) {
|
|
AdminToast.error('Insufficient wallet balance');
|
|
return;
|
|
}
|
|
}
|
|
|
|
setLoading(this, true);
|
|
|
|
try {
|
|
const response = await fetch('/api/pos-order.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
items: POS.cart.map(item => ({
|
|
product_id: item.id,
|
|
name: item.name,
|
|
price: item.price,
|
|
quantity: item.quantity,
|
|
total: item.price * item.quantity
|
|
})),
|
|
payment_method: POS.paymentMethod,
|
|
notes: document.getElementById('order-notes').value,
|
|
total: total,
|
|
discount: discount,
|
|
coupon_code: POS.coupon?.code,
|
|
customer_id: POS.customer?.customer_id,
|
|
customer_email: POS.customer?.email,
|
|
customer_name: POS.customer?.name
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.error) {
|
|
AdminToast.error(data.error);
|
|
} else {
|
|
Modal.close('paymentModal');
|
|
showReceipt(data, POS.paymentMethod === 'cash' ? parseFloat(document.getElementById('cash-received').value) : total);
|
|
|
|
// Reset
|
|
POS.cart = [];
|
|
POS.coupon = null;
|
|
POS.manualDiscount = 0;
|
|
removeCustomer();
|
|
document.getElementById('coupon-input').value = '';
|
|
document.getElementById('manual-discount').value = '';
|
|
document.getElementById('applied-discounts').innerHTML = '';
|
|
document.getElementById('order-notes').value = '';
|
|
renderCart();
|
|
|
|
AdminToast.success('Order completed! #' + data.order_number);
|
|
}
|
|
} catch (err) {
|
|
AdminToast.error('Failed to process order');
|
|
console.error(err);
|
|
} finally {
|
|
setLoading(this, false);
|
|
}
|
|
});
|
|
|
|
// Show receipt
|
|
function showReceipt(orderData, amountPaid) {
|
|
const { subtotal, discount, tax, total } = calculateTotals();
|
|
const change = amountPaid - total;
|
|
|
|
const receiptHtml = `
|
|
<div class="receipt">
|
|
<div class="receipt-header">
|
|
<h2>Tom's Java Jive</h2>
|
|
<p>Order #${orderData.order_number}</p>
|
|
<p>${new Date().toLocaleString()}</p>
|
|
</div>
|
|
<div class="receipt-items">
|
|
${orderData.items.map(item => `
|
|
<div class="receipt-item">
|
|
<span>${item.quantity}x ${item.name}</span>
|
|
<span>$${item.total.toFixed(2)}</span>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
<div class="receipt-totals">
|
|
<div class="receipt-total-row">
|
|
<span>Subtotal</span>
|
|
<span>$${subtotal.toFixed(2)}</span>
|
|
</div>
|
|
${discount > 0 ? `
|
|
<div class="receipt-total-row">
|
|
<span>Discount</span>
|
|
<span>-$${discount.toFixed(2)}</span>
|
|
</div>
|
|
` : ''}
|
|
<div class="receipt-total-row">
|
|
<span>Tax</span>
|
|
<span>$${tax.toFixed(2)}</span>
|
|
</div>
|
|
<div class="receipt-total-row grand-total">
|
|
<span>TOTAL</span>
|
|
<span>$${total.toFixed(2)}</span>
|
|
</div>
|
|
<div class="receipt-total-row">
|
|
<span>Paid (${POS.paymentMethod})</span>
|
|
<span>$${amountPaid.toFixed(2)}</span>
|
|
</div>
|
|
${change > 0 ? `
|
|
<div class="receipt-total-row">
|
|
<span>Change</span>
|
|
<span>$${change.toFixed(2)}</span>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
<div class="receipt-footer">
|
|
<p>Thank you for your purchase!</p>
|
|
<p>tomsjavajive.com</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('receiptContent').innerHTML = receiptHtml;
|
|
Modal.open('receiptModal');
|
|
}
|
|
|
|
function printReceipt() {
|
|
window.print();
|
|
}
|
|
|
|
// Utility
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text || '';
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Initialize
|
|
renderCart();
|
|
updateHeldCount();
|
|
</script>
|
|
|
|
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|