39728-vm/pos.php
2026-04-20 16:02:36 +00:00

1069 lines
38 KiB
PHP

<?php
$saleMode = 'pos';
require_once __DIR__ . '/includes/app.php';
$user = require_permission('pos', 'show');
$pageTitle = tr('نقاط البيع', 'Smart POS');
$activeNav = 'pos';
$error = '';
$catalog = catalog();
$allowedBranches = get_user_branches($user);
try {
$pdo = db();
$categories = $pdo->query('SELECT id, name_ar, name_en FROM categories ORDER BY name_ar ASC')->fetchAll();
$customers = $pdo->query('SELECT id, name, phone FROM customers ORDER BY name ASC')->fetchAll();
} catch (Throwable $e) {
$categories = [];
$customers = [];
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$branchCode = trim((string) ($_POST['branch_code'] ?? ''));
$customerId = isset($_POST['customer_id']) && $_POST['customer_id'] !== '' ? (int)$_POST['customer_id'] : null;
$customerName = trim((string) ($_POST['customer_name'] ?? ''));
$paymentMethod = trim((string) ($_POST['payment_method'] ?? 'cash'));
$paymentStatus = ($paymentMethod === 'pay_later') ? 'unpaid' : 'paid';
$notes = trim((string) ($_POST['notes'] ?? ''));
$cartJson = (string) ($_POST['cart_json'] ?? '[]');
$items = json_decode($cartJson, true);
if (!in_array($branchCode, $allowedBranches, true)) {
$error = tr('اختر فرعاً صالحاً لهذه الصلاحية.', 'Choose a valid branch for this role.');
} elseif (!in_array($paymentMethod, ['cash', 'card', 'transfer', 'pay_later'], true)) {
$error = tr('اختر طريقة دفع صحيحة.', 'Choose a valid payment method.');
} elseif ($paymentMethod === 'pay_later' && !$customerId) {
$error = tr('يجب اختيار عميل مسجل للدفع الآجل.', 'You must select a registered customer for pay later.');
} elseif (!is_array($items) || $items === []) {
$error = tr('أضف صنفاً واحداً على الأقل إلى السلة.', 'Add at least one item to the cart.');
} else {
$normalized = [];
$subtotal = 0.0;
$totalVat = 0.0;
$itemCount = 0;
foreach ($items as $item) {
$sku = (string) ($item['sku'] ?? '');
$qty = (int) ($item['qty'] ?? 0);
if (!isset($catalog[$sku]) || $qty < 1) {
continue;
}
$product = $catalog[$sku];
$price = (float) $product['price'];
$lineTotal = $price * $qty;
$normalized[] = [
'sku' => $sku,
'name_ar' => $product['name_ar'],
'name_en' => $product['name_en'],
'qty' => $qty,
'price' => $price,
'line_total' => $lineTotal,
];
$subtotal += $lineTotal;
$vatPercent = (float) ($product['vat'] ?? 0);
$itemVat = $lineTotal * ($vatPercent / 100);
$totalVat += $itemVat;
$itemCount += $qty;
}
if ($normalized === []) {
$error = tr('السلة غير صالحة بعد التحقق من الأصناف.', 'The cart is invalid after product validation.');
} else {
$cashierName = current_lang() === 'ar' ? $user['name_ar'] : $user['name_en'];
$saleId = create_sale([
'receipt_no' => receipt_code(),
'sale_mode' => $saleMode,
'branch_code' => $branchCode,
'cashier_username' => $user['username'],
'cashier_name' => $cashierName,
'role_name' => $user['role'],
'customer_id' => $customerId,
'customer_name' => $customerName !== '' ? $customerName : null,
'payment_method' => $paymentMethod,
'payment_status' => $paymentStatus,
'items' => $normalized,
'item_count' => $itemCount,
'subtotal' => $subtotal,
'vat_amount' => $totalVat,
'total_amount' => $subtotal + $totalVat,
'notes' => $notes !== '' ? $notes : null,
]);
set_flash('success', tr('تم حفظ عملية POS بنجاح.', 'POS sale saved successfully.'));
redirect_to('print_receipt.php', ['id' => $saleId]);
}
}
}
require __DIR__ . '/includes/header.php';
?>
<style>
/* Modern POS Styles */
.pos-wrapper {
height: calc(100vh - 140px); /* Adjust based on your header */
min-height: 600px;
display: flex;
gap: 1.5rem;
}
.pos-products-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.pos-cart-area {
width: 380px;
background: #fff;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
display: flex;
flex-direction: column;
overflow: hidden;
flex-shrink: 0;
}
/* Categories Scroll */
.cat-scroll-container {
display: flex;
gap: 0.75rem;
overflow-x: auto;
padding-bottom: 0.5rem;
scrollbar-width: none; /* Firefox */
}
.cat-scroll-container::-webkit-scrollbar {
display: none; /* Chrome */
}
.cat-btn {
white-space: nowrap;
border-radius: 20px;
padding: 0.5rem 1.25rem;
font-weight: 500;
transition: all 0.2s;
background: #fff;
border: 1px solid #e9ecef;
color: #495057;
cursor: pointer;
}
.cat-btn:hover {
background: #f8f9fa;
}
.cat-btn.active {
background: linear-gradient(90deg, #0d6efd, #0dcaf0);
color: #fff;
border-color: transparent;
box-shadow: 0 4px 10px rgba(13, 110, 253, 0.2);
}
/* Products Grid */
.products-grid {
flex: 1;
overflow-y: auto;
padding: 0.25rem 0.5rem 0.5rem 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 1rem;
align-content: start;
}
.product-card {
background: #fff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
cursor: pointer;
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
border: 1px solid #dbe4f0;
position: relative;
user-select: none;
display: flex;
flex-direction: column;
min-height: 252px;
color: #1f2937;
}
.product-card[hidden] {
display: none !important;
}
.product-card:hover {
transform: translateY(-3px);
box-shadow: 0 14px 28px rgba(13, 110, 253, 0.12);
border-color: #0d6efd;
}
.product-card:active {
transform: translateY(-1px);
}
.product-img-wrapper {
height: 132px;
width: 100%;
background: linear-gradient(180deg, #f8fbff 0%, #eef4fb 100%);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border-bottom: 1px solid #edf2f7;
}
.product-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.product-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.35rem;
color: #98a2b3;
font-size: 1.9rem;
}
.product-placeholder-label {
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.01em;
color: #6b7280;
}
.product-info {
padding: 0.85rem;
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: left;
gap: 0.45rem;
flex: 1;
}
.product-badges {
display: flex;
justify-content: flex-start;
gap: 0.35rem;
flex-wrap: wrap;
margin-bottom: 0.1rem;
}
.product-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.18rem 0.5rem;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
line-height: 1;
}
.product-badge-new {
background: rgba(13, 202, 240, 0.14);
color: #087990;
}
.product-badge-no-image {
background: rgba(108, 117, 125, 0.12);
color: #6c757d;
}
.product-title {
font-size: 0.95rem;
font-weight: 700;
color: #1f2937;
line-height: 1.35;
min-height: 2.6em;
max-height: 2.6em;
overflow: hidden;
word-break: break-word;
}
.product-meta {
display: flex;
flex-direction: column;
gap: 0.2rem;
color: #667085;
margin-bottom: auto;
width: 100%;
}
.product-sku,
.product-created {
font-size: 0.78rem;
line-height: 1.25;
word-break: break-word;
}
.product-price {
font-weight: 800;
color: #0d6efd;
font-size: 1.05rem;
margin-top: 0.15rem;
}
[dir="rtl"] .product-info {
align-items: flex-end;
text-align: right;
}
[dir="rtl"] .product-badges {
justify-content: flex-end;
}
/* Cart Styles */
.cart-header {
background: linear-gradient(90deg, #0d6efd, #0dcaf0);
color: #fff;
padding: 1rem 1.25rem;
}
.cart-items {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.cart-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px dashed #e9ecef;
}
.cart-item-info {
flex: 1;
padding-right: 1rem;
}
.cart-item-title {
font-weight: 600;
font-size: 0.9rem;
color: #343a40;
}
.cart-item-price {
color: #6c757d;
font-size: 0.85rem;
}
.cart-item-controls {
display: flex;
align-items: center;
gap: 0.5rem;
background: #f8f9fa;
border-radius: 20px;
padding: 0.25rem;
}
.cart-btn {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: none;
background: #fff;
color: #495057;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
transition: all 0.2s;
}
.cart-btn:hover {
background: #e9ecef;
}
.cart-qty {
font-weight: 600;
font-size: 0.9rem;
min-width: 20px;
text-align: center;
}
.cart-footer {
padding: 1.25rem;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
}
.summary-row {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.95rem;
color: #495057;
}
.summary-total {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
font-size: 1.25rem;
font-weight: 700;
color: #212529;
padding-top: 0.5rem;
border-top: 1px solid #dee2e6;
}
/* Pay / Action Buttons */
.btn-pay {
background: linear-gradient(90deg, #198754, #20c997);
border: none;
color: white;
font-size: 1.1rem;
font-weight: 600;
padding: 0.75rem;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(25, 135, 84, 0.2);
transition: all 0.2s;
}
.btn-pay:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(25, 135, 84, 0.3);
}
.btn-pay:disabled {
background: #adb5bd;
transform: none;
box-shadow: none;
}
.action-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-bottom: 1rem;
}
[dir="ltr"] .cart-item-info {
padding-right: 0;
padding-left: 1rem;
}
</style>
<?php if ($error !== ''): ?>
<div class="alert alert-danger shadow-sm border-0 rounded-3 mb-4"><?= h($error) ?></div>
<?php endif; ?>
<div class="pos-wrapper">
<!-- Left Area: Products -->
<div class="pos-products-area">
<!-- Top Bar: Search & Hold -->
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<div class="d-flex gap-2 flex-wrap">
<div class="position-relative" style="width: 200px;">
<input type="text" id="posBarcode" class="form-control rounded-pill ps-4 border-primary shadow-sm" placeholder="<?= h(tr('الباركود...', 'Barcode...')) ?>" autocomplete="off" autofocus>
<i class="bi bi-upc-scan position-absolute top-50 translate-middle-y text-primary" style="left: 15px;"></i>
</div>
<div class="position-relative" style="width: 250px;">
<input type="text" id="posSearch" class="form-control rounded-pill ps-4" placeholder="<?= h(tr('بحث بالاسم...', 'Search by name...')) ?>" autocomplete="off">
<i class="bi bi-search position-absolute top-50 translate-middle-y text-muted" style="left: 15px;"></i>
</div>
</div>
<div>
<button class="btn btn-outline-primary rounded-pill px-4 shadow-sm" onclick="openHeldOrdersModal()">
<i class="bi bi-clock-history me-1"></i> <span id="heldOrdersCount">0</span> <?= h(tr('قيد الانتظار', 'Held Orders')) ?>
</button>
</div>
</div>
<!-- Categories -->
<div class="cat-scroll-container mb-4" id="catContainer">
<button class="cat-btn active" data-cat="all" onclick="filterCat('all')"><?= h(tr('الكل', 'All')) ?></button>
<?php foreach($categories as $cat):
$catId = h($cat['id']);
$catName = h(current_lang() === 'ar' ? $cat['name_ar'] : $cat['name_en']);
?>
<button class="cat-btn" data-cat="<?= $catId ?>" onclick="filterCat('<?= $catId ?>')"><?= $catName ?></button>
<?php endforeach; ?>
</div>
<!-- Grid -->
<div class="products-grid" id="productsGrid">
<?php $productIndex = 0; foreach ($catalog as $sku => $item):
$itemSkuRaw = (string) $sku;
$primaryNameRaw = trim((string) (current_lang() === 'ar' ? ($item['name_ar'] ?? '') : ($item['name_en'] ?? '')));
$fallbackNameRaw = trim((string) (current_lang() === 'ar' ? ($item['name_en'] ?? '') : ($item['name_ar'] ?? '')));
$itemNameRaw = $primaryNameRaw !== ''
? $primaryNameRaw
: ($fallbackNameRaw !== '' ? $fallbackNameRaw : (tr('صنف ' . $itemSkuRaw, 'Item ' . $itemSkuRaw)));
$searchBits = [
(string) ($item['name_ar'] ?? ''),
(string) ($item['name_en'] ?? ''),
$itemSkuRaw,
];
$searchTextRaw = implode(' ', array_filter($searchBits, static fn($value) => trim((string) $value) !== ''));
$searchText = function_exists('mb_strtolower')
? mb_strtolower($searchTextRaw, 'UTF-8')
: strtolower($searchTextRaw);
$itemSku = h($itemSkuRaw);
$itemName = h($itemNameRaw);
$itemPrice = h($item['price']);
$itemCat = h($item['category_id'] ?? '');
$imageUrl = !empty($item['image_url']) ? h($item['image_url']) : '';
$createdAtRaw = (string)($item['created_at'] ?? '');
$createdAtStamp = $createdAtRaw !== '' ? strtotime($createdAtRaw) : false;
$isRecentlyAdded = $createdAtStamp && $createdAtStamp >= strtotime('-7 days');
$createdLabel = $createdAtStamp ? date('Y-m-d', $createdAtStamp) : '';
?>
<div class="product-card" data-sku="<?= $itemSku ?>" data-name="<?= $itemName ?>" data-price="<?= $itemPrice ?>" data-cat="<?= $itemCat ?>" data-search="<?= h($searchText) ?>" data-created="<?= h($createdAtRaw) ?>" data-index="<?= $productIndex++ ?>" onclick="addToCart('<?= $itemSku ?>')">
<div class="product-img-wrapper">
<?php if (!empty($imageUrl)):
$imgAlt = $itemName;
?>
<img src="<?= $imageUrl ?>" alt="<?= $imgAlt ?>" class="product-img" loading="lazy">
<?php else: ?>
<div class="product-placeholder" aria-hidden="true">
<i class="bi bi-image"></i>
<span class="product-placeholder-label"><?= h(tr('بدون صورة', 'No image')) ?></span>
</div>
<?php endif; ?>
</div>
<div class="product-info">
<div class="product-badges">
<?php if ($isRecentlyAdded): ?>
<span class="product-badge product-badge-new"><?= h(tr('جديد', 'New')) ?></span>
<?php endif; ?>
<?php if (empty($imageUrl)): ?>
<span class="product-badge product-badge-no-image"><?= h(tr('بدون صورة', 'No image')) ?></span>
<?php endif; ?>
</div>
<div class="product-title" title="<?= $itemName ?>"><?= $itemName ?></div>
<div class="product-price"><?= h(currency((float)$item['price'])) ?></div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Right Area: Smart Cart -->
<div class="pos-cart-area">
<div class="cart-header d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-bold"><i class="bi bi-cart3 me-2"></i><?= h(tr('سلة المشتريات', 'Shopping Cart')) ?></h5>
<span class="badge bg-light text-primary rounded-pill px-2" id="cartBadgeCount">0</span>
</div>
<div class="p-3 bg-light border-bottom">
<select class="form-select border-0 shadow-sm rounded-3 mb-2" id="posBranch" <?= count($allowedBranches) === 1 ? 'disabled' : '' ?> required>
<?php foreach ($allowedBranches as $branchCode):
$branchLabel = h(branch_label($branchCode));
?>
<option value="<?= h($branchCode) ?>"><?= $branchLabel ?></option>
<?php endforeach; ?>
</select>
<div class="input-group position-relative">
<input type="text" id="posCustomer" class="form-control border-0 shadow-sm rounded-start-3" placeholder="<?= h(tr('بحث عن عميل (اسم أو هاتف)', 'Search Customer (Name or Phone)')) ?>" autocomplete="off">
<button class="btn btn-primary border-0 shadow-sm rounded-end-3" onclick="openNewCustomerModal()" type="button" title="<?= h(tr('إضافة عميل', 'Add Customer')) ?>">
<i class="bi bi-person-plus-fill"></i>
</button>
<div id="customerDropdown" class="list-group position-absolute w-100 shadow-lg d-none" style="top: 100%; left: 0; z-index: 1050; max-height: 200px; overflow-y: auto; border-radius: 8px;"></div>
</div>
</div>
<div class="cart-items" id="cartItemsList">
<!-- Items rendered via JS -->
<div class="text-center text-muted mt-5 pt-4">
<i class="bi bi-basket2 fs-1 d-block mb-3 opacity-50"></i>
<p><?= h(tr('السلة فارغة، اختر بعض الأصناف.', 'Cart is empty, pick some items.')) ?></p>
</div>
</div>
<div class="cart-footer">
<div class="summary-row">
<span><?= h(tr('المجموع الفرعي', 'Subtotal')) ?></span>
<span id="cartSubtotalVal">0.000</span>
</div>
<div class="summary-row">
<span><?= h(tr('الضريبة (مضافة)', 'VAT (Added)')) ?></span>
<span id="cartVatVal" class="text-muted">0.000</span>
</div>
<div class="summary-total">
<span><?= h(tr('الإجمالي', 'Total')) ?></span>
<span class="text-primary" id="cartTotalVal">0.000</span>
</div>
<div class="action-grid">
<button class="btn btn-outline-warning rounded-3 fw-semibold" onclick="holdCart()" id="btnHold" disabled>
<i class="bi bi-pause-circle"></i> <?= h(tr('تعليق', 'Hold')) ?>
</button>
<button class="btn btn-outline-danger rounded-3 fw-semibold" onclick="clearCart()" id="btnClear" disabled>
<i class="bi bi-trash"></i> <?= h(tr('إلغاء', 'Clear')) ?>
</button>
</div>
<button class="btn w-100 btn-pay" id="btnPay" onclick="openPaymentModal()" disabled>
<i class="bi bi-wallet2 me-2"></i> <?= h(tr('دفع وإصدار', 'Pay & Checkout')) ?>
</button>
</div>
</div>
</div>
<!-- Hidden Form for Submission -->
<form method="POST" id="checkoutForm" style="display: none;">
<input type="hidden" name="branch_code" id="inputBranch">
<input type="hidden" name="customer_id" id="inputCustomerId">
<input type="hidden" name="customer_name" id="inputCustomer">
<input type="hidden" name="payment_method" id="inputPayment">
<input type="hidden" name="notes" id="inputNotes">
<input type="hidden" name="cart_json" id="inputCart">
</form>
<!-- Payment Modal -->
<div class="modal fade" id="paymentModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content border-0 shadow-lg" style="border-radius: 16px;">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold"><?= h(tr('إتمام الدفع', 'Complete Payment')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center pt-2">
<h2 class="text-primary fw-bold mb-4" id="modalTotalAmount">0.000</h2>
<div class="d-grid gap-3">
<button class="btn btn-lg btn-outline-success rounded-pill fw-semibold" onclick="submitSale('cash')">
<i class="bi bi-cash-coin me-2"></i> <?= h(tr('نقداً', 'Cash')) ?>
</button>
<button class="btn btn-lg btn-outline-primary rounded-pill fw-semibold" onclick="submitSale('card')">
<i class="bi bi-credit-card me-2"></i> <?= h(tr('بطاقة', 'Card')) ?>
</button>
<button class="btn btn-lg btn-outline-secondary rounded-pill fw-semibold" onclick="submitSale('transfer')">
<i class="bi bi-phone me-2"></i> <?= h(tr('تحويل بنكي', 'Transfer')) ?>
</button>
<button class="btn btn-lg btn-outline-warning rounded-pill fw-semibold" onclick="submitSale('pay_later')">
<i class="bi bi-clock-history me-2"></i> <?= h(tr('آجل', 'Pay Later')) ?>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- New Customer Modal -->
<div class="modal fade" id="newCustomerModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content border-0 shadow-lg" style="border-radius: 16px;">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold"><?= h(tr('إضافة عميل', 'Add Customer')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label text-muted small mb-1"><?= h(tr('الاسم', 'Name')) ?> <span class="text-danger">*</span></label>
<input type="text" id="ncName" class="form-control rounded-3">
</div>
<div class="mb-3">
<label class="form-label text-muted small mb-1"><?= h(tr('رقم الهاتف', 'Phone')) ?></label>
<input type="text" id="ncPhone" class="form-control rounded-3" dir="ltr">
</div>
<div class="d-grid mt-4">
<button class="btn btn-primary rounded-pill fw-semibold shadow-sm" onclick="saveNewCustomer()"><?= h(tr('حفظ العميل', 'Save Customer')) ?></button>
</div>
</div>
</div>
</div>
</div>
<!-- Held Orders Modal -->
<div class="modal fade" id="heldOrdersModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg" style="border-radius: 16px;">
<div class="modal-header bg-light border-0">
<h5 class="modal-title fw-bold"><i class="bi bi-clock-history me-2 text-primary"></i><?= h(tr('الطلبات المعلقة', 'Held Orders')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0">
<div class="list-group list-group-flush" id="heldOrdersList">
<!-- Rendered by JS -->
</div>
</div>
</div>
</div>
</div>
<script>
let cart = {};
let catalogData = <?= json_encode($catalog, JSON_UNESCAPED_UNICODE) ?>;
let customersData = <?= json_encode($customers, JSON_UNESCAPED_UNICODE) ?>;
let currencyLabel = '<?= h(tr('ر.ع', 'OMR')) ?>';
// Customer Autocomplete & Add Logic
const custInput = document.getElementById('posCustomer');
const custDropdown = document.getElementById('customerDropdown');
custInput.addEventListener('input', function() {
const q = this.value.toLowerCase().trim();
custDropdown.innerHTML = '';
if (q.length < 2) {
custDropdown.classList.add('d-none');
return;
}
const matches = customersData.filter(c =>
c.name.toLowerCase().includes(q) ||
(c.phone && c.phone.toLowerCase().includes(q))
).slice(0, 5);
if (matches.length > 0) {
matches.forEach(c => {
const a = document.createElement('a');
a.className = 'list-group-item list-group-item-action cursor-pointer border-0 border-bottom';
a.innerHTML = `<strong>${c.name}</strong> ${c.phone ? '<small class="text-muted ms-2">'+c.phone+'</small>' : ''}`;
a.onclick = function() {
custInput.value = c.name + (c.phone ? ' - ' + c.phone : '');
custInput.dataset.id = c.id;
custDropdown.classList.add('d-none');
};
custDropdown.appendChild(a);
});
custDropdown.classList.remove('d-none');
} else {
custDropdown.classList.add('d-none');
}
});
document.addEventListener('click', function(e) {
if (!custInput.contains(e.target) && !custDropdown.contains(e.target)) {
custDropdown.classList.add('d-none');
}
});
let newCustomerModalObj = null;
function openNewCustomerModal() {
if (!newCustomerModalObj) {
newCustomerModalObj = new bootstrap.Modal(document.getElementById('newCustomerModal'));
}
document.getElementById('ncName').value = '';
document.getElementById('ncPhone').value = '';
newCustomerModalObj.show();
}
async function saveNewCustomer() {
const name = document.getElementById('ncName').value.trim();
const phone = document.getElementById('ncPhone').value.trim();
if (!name) {
Swal.fire({icon: 'warning', text: '<?= h(tr('الاسم مطلوب', 'Name is required')) ?>'});
return;
}
const formData = new FormData();
formData.append('name', name);
formData.append('phone', phone);
try {
const res = await fetch('api/customers.php', {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.success) {
customersData.push(data.customer);
custInput.value = data.customer.name + (data.customer.phone ? ' - ' + data.customer.phone : '');
custInput.dataset.id = data.customer.id;
newCustomerModalObj.hide();
const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 2000 });
Toast.fire({ icon: 'success', title: '<?= h(tr('تم إضافة العميل', 'Customer added')) ?>' });
} else {
Swal.fire({icon: 'warning', text: data.error});
}
} catch(err) {
Swal.fire({icon: 'warning', text: 'Error saving customer'});
}
}
// Product Grid Filtering & Searching
function filterCat(catId) {
document.querySelectorAll('.cat-btn').forEach(btn => btn.classList.remove('active'));
document.querySelector(`.cat-btn[data-cat="${catId}"]`).classList.add('active');
applyFilters();
}
document.getElementById('posSearch').addEventListener('input', applyFilters);
document.getElementById('posBarcode').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
const sku = this.value.trim();
if (sku === '') return;
if (catalogData[sku]) {
addToCart(sku);
} else {
const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 2000 });
Toast.fire({ icon: 'error', title: '<?= h(tr('الصنف غير موجود', 'Item not found')) ?>' });
}
this.value = '';
}
});
const INITIAL_PRODUCT_LIMIT = 100;
function applyFilters() {
const q = document.getElementById('posSearch').value.toLowerCase().trim();
const activeCat = document.querySelector('.cat-btn.active').dataset.cat;
let visibleCount = 0;
document.querySelectorAll('.product-card').forEach(card => {
const searchable = (card.dataset.search || card.dataset.name || '').toLowerCase();
const cat = card.dataset.cat;
const matchesSearch = q === '' || searchable.includes(q);
const matchesCat = (activeCat === 'all' || cat === activeCat);
const matches = matchesSearch && matchesCat;
const withinInitialLimit = q !== '' || visibleCount < INITIAL_PRODUCT_LIMIT;
card.hidden = !(matches && withinInitialLimit);
if (matches) {
visibleCount += 1;
}
});
}
applyFilters();
// Cart Logic
function addToCart(sku) {
if (!catalogData[sku]) return;
if (cart[sku]) {
cart[sku].qty += 1;
} else {
cart[sku] = {
sku: sku,
name: document.querySelector(`.product-card[data-sku="${sku}"]`).dataset.name,
price: parseFloat(document.querySelector(`.product-card[data-sku="${sku}"]`).dataset.price),
qty: 1
};
}
renderCart();
}
function updateQty(sku, delta) {
if (!cart[sku]) return;
cart[sku].qty += delta;
if (cart[sku].qty <= 0) {
delete cart[sku];
}
renderCart();
}
function renderCart() {
const list = document.getElementById('cartItemsList');
const badge = document.getElementById('cartBadgeCount');
const subVal = document.getElementById('cartSubtotalVal');
const vatVal = document.getElementById('cartVatVal');
const totalVal = document.getElementById('cartTotalVal');
const btnPay = document.getElementById('btnPay');
const btnHold = document.getElementById('btnHold');
const btnClear = document.getElementById('btnClear');
list.innerHTML = '';
let total = 0;
let count = 0;
let totalVat = 0;
const skus = Object.keys(cart);
if (skus.length === 0) {
list.innerHTML = `
<div class="text-center text-muted mt-5 pt-4">
<i class="bi bi-basket2 fs-1 d-block mb-3 opacity-50"></i>
<p><?= h(tr('السلة فارغة، اختر بعض الأصناف.', 'Cart is empty, pick some items.')) ?></p>
</div>
`;
badge.innerText = '0';
subVal.innerText = `0.000 ${currencyLabel}`;
vatVal.innerText = `0.000`;
totalVal.innerText = `0.000 ${currencyLabel}`;
btnPay.disabled = true;
btnHold.disabled = true;
btnClear.disabled = true;
return;
}
skus.forEach(sku => {
const item = cart[sku];
const lineTotal = item.price * item.qty;
const vatPercent = parseFloat(catalogData[sku].vat) || 0;
const itemVat = lineTotal * (vatPercent / 100);
totalVat += itemVat;
total += lineTotal;
count += item.qty;
list.innerHTML += `
<div class="cart-item">
<div class="cart-item-info">
<div class="cart-item-title">
${item.name}
</div>
<div class="cart-item-price">
${item.price.toFixed(3)} ${currencyLabel}
</div>
</div>
<div class="cart-item-controls">
<button class="cart-btn" onclick="updateQty('${sku}', -1)"><i class="bi bi-dash"></i></button>
<span class="cart-qty">${item.qty}</span>
<button class="cart-btn" onclick="updateQty('${sku}', 1)"><i class="bi bi-plus"></i></button>
</div>
</div>
`;
});
badge.innerText = count;
const subtotal = total;
const finalTotal = subtotal + totalVat;
const totalStr = `${finalTotal.toFixed(3)} ${currencyLabel}`;
subVal.innerText = subtotal.toFixed(3) + ' ' + currencyLabel;
vatVal.innerText = totalVat.toFixed(3);
totalVal.innerText = totalStr;
document.getElementById('modalTotalAmount').innerText = totalStr;
btnPay.disabled = false;
btnHold.disabled = false;
btnClear.disabled = false;
}
function clearCart() {
cart = {};
document.getElementById('posCustomer').value = '';
delete document.getElementById('posCustomer').dataset.id;
renderCart();
}
// Payment & Checkout
let paymentModalObj = null;
document.addEventListener('DOMContentLoaded', () => {
paymentModalObj = new bootstrap.Modal(document.getElementById('paymentModal'));
updateHeldOrdersCount();
});
function openPaymentModal() {
if (Object.keys(cart).length === 0) return;
paymentModalObj.show();
}
function submitSale(method) {
const branch = document.getElementById('posBranch').value || '<?= h($allowedBranches[0] ?? '') ?>';
const customer = document.getElementById('posCustomer').value;
const customerId = document.getElementById('posCustomer').dataset.id || '';
if (method === 'pay_later' && !customerId) {
Swal.fire({icon: 'warning', text: '<?= h(tr('يجب اختيار عميل مسجل للدفع الآجل.', 'You must select a registered customer for pay later.')) ?>'});
return;
}
const itemsArr = Object.values(cart).map(item => ({
sku: item.sku,
qty: item.qty
}));
document.getElementById('inputBranch').value = branch;
document.getElementById('inputCustomerId').value = customerId;
document.getElementById('inputCustomer').value = customer;
document.getElementById('inputPayment').value = method;
document.getElementById('inputCart').value = JSON.stringify(itemsArr);
document.getElementById('checkoutForm').submit();
}
// Held Orders Logic (localStorage)
function getHeldOrders() {
const raw = localStorage.getItem('posHeldOrders');
return raw ? JSON.parse(raw) : [];
}
function saveHeldOrders(orders) {
localStorage.setItem('posHeldOrders', JSON.stringify(orders));
updateHeldOrdersCount();
}
function updateHeldOrdersCount() {
const orders = getHeldOrders();
document.getElementById('heldOrdersCount').innerText = orders.length;
}
function holdCart() {
if (Object.keys(cart).length === 0) return;
const orders = getHeldOrders();
const orderName = document.getElementById('posCustomer').value || `<?= h(tr('عميل غير معروف', 'Unknown')) ?> #${Math.floor(Math.random()*1000)}`;
const branch = document.getElementById('posBranch').value;
const newOrder = {
id: Date.now(),
name: orderName,
branch: branch,
cart: JSON.parse(JSON.stringify(cart)),
time: new Date().toLocaleTimeString()
};
orders.push(newOrder);
saveHeldOrders(orders);
// Clear current cart via toast
clearCart();
const Toast = Swal.mixin({
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 2000
});
Toast.fire({
icon: 'success',
title: '<?= h(tr('تم تعليق الطلب', 'Order held successfully')) ?>'
});
}
let heldOrdersModalObj = null;
function openHeldOrdersModal() {
if (!heldOrdersModalObj) {
heldOrdersModalObj = new bootstrap.Modal(document.getElementById('heldOrdersModal'));
}
const list = document.getElementById('heldOrdersList');
const orders = getHeldOrders();
list.innerHTML = '';
if (orders.length === 0) {
list.innerHTML = `<div class="p-4 text-center text-muted"><?= h(tr('لا توجد طلبات معلقة', 'No held orders')) ?></div>`;
} else {
orders.forEach((order, index) => {
const count = Object.values(order.cart).reduce((sum, i) => sum + i.qty, 0);
list.innerHTML += `
<div class="list-group-item d-flex justify-content-between align-items-center p-3">
<div>
<h6 class="mb-1 fw-bold">
${order.name}
</h6>
<small class="text-muted"><i class="bi bi-clock me-1"></i>${order.time} &bull; ${count} <?= h(tr('قطعة', 'items')) ?></small>
</div>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-primary rounded-pill px-3" onclick="resumeOrder(${index})"><?= h(tr('استعادة', 'Resume')) ?></button>
<button class="btn btn-sm btn-outline-danger rounded-pill" onclick="deleteHeldOrder(${index})"><i class="bi bi-trash"></i></button>
</div>
</div>
`;
});
}
heldOrdersModalObj.show();
}
function resumeOrder(index) {
const orders = getHeldOrders();
if (!orders[index]) return;
const order = orders[index];
// Warn if cart is not empty
if (Object.keys(cart).length > 0) {
Swal.fire({
title: '<?= h(tr("تنبيه", "Alert")) ?>',
text: '<?= h(tr("السلة الحالية غير فارغة، هل تريد استبدالها؟", "Current cart is not empty, replace it?")) ?>',
icon: 'warning',
showCancelButton: true,
confirmButtonText: '<?= h(tr("نعم", "Yes")) ?>',
cancelButtonText: '<?= h(tr("إلغاء", "Cancel")) ?>'
}).then((result) => {
if (result.isConfirmed) {
doResumeOrder(index, order, orders);
}
});
} else {
doResumeOrder(index, order, orders);
}
}
function doResumeOrder(index, order, orders) {
cart = order.cart;
document.getElementById('posCustomer').value = order.name.includes('#') ? '' : order.name;
if (document.getElementById('posBranch').querySelector(`option[value="${order.branch}"]`)) {
document.getElementById('posBranch').value = order.branch;
}
orders.splice(index, 1);
saveHeldOrders(orders);
renderCart();
heldOrdersModalObj.hide();
}
function deleteHeldOrder(index) {
const orders = getHeldOrders();
orders.splice(index, 1);
saveHeldOrders(orders);
openHeldOrdersModal(); // re-render
}
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>