Autosave: 20260302-110501
This commit is contained in:
parent
ab23c4540e
commit
62b332799d
BIN
assets/images/items/item_69a5695c9ca064.49414552.jpg
Normal file
BIN
assets/images/items/item_69a5695c9ca064.49414552.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
BIN
assets/images/items/item_69a5697ac55c21.80460234.jpg
Normal file
BIN
assets/images/items/item_69a5697ac55c21.80460234.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
assets/images/items/item_69a569a491f0e1.37331888.jpg
Normal file
BIN
assets/images/items/item_69a569a491f0e1.37331888.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
5
db/migrations/05_simplify_prices_index.sql
Normal file
5
db/migrations/05_simplify_prices_index.sql
Normal file
@ -0,0 +1,5 @@
|
||||
-- Drop the old index with variant_id
|
||||
DROP INDEX unique_price ON prices;
|
||||
|
||||
-- Add a new unique index without variant_id
|
||||
CREATE UNIQUE INDEX unique_price ON prices (branch_id, item_id, service_id);
|
||||
751
pos.php
751
pos.php
@ -2,7 +2,7 @@
|
||||
$title = 'pos';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
$branch_id = $_SESSION['branch_id'];
|
||||
$branch_id = $_SESSION['branch_id'] ?? 1;
|
||||
|
||||
// Get all categories
|
||||
$categories = db()->query("SELECT * FROM categories ORDER BY name_en ASC")->fetchAll();
|
||||
@ -15,166 +15,207 @@ $stmt = db()->prepare("SELECT i.*, c.name_en as cat_en, c.name_ar as cat_ar
|
||||
$stmt->execute();
|
||||
$items_raw = $stmt->fetchAll();
|
||||
|
||||
// Get all services and prices for this branch
|
||||
$stmt = db()->prepare("SELECT p.item_id, p.service_id, p.price,
|
||||
s.name_en as service_en, s.name_ar as service_ar
|
||||
// Get all services
|
||||
$services_raw = db()->query("SELECT * FROM services ORDER BY name_en ASC")->fetchAll();
|
||||
|
||||
// Get all prices for this branch
|
||||
$stmt = db()->prepare("SELECT p.item_id, p.service_id, p.price
|
||||
FROM prices p
|
||||
JOIN services s ON p.service_id = s.id
|
||||
WHERE p.branch_id = ?");
|
||||
$stmt->execute([$branch_id]);
|
||||
$all_prices = $stmt->fetchAll();
|
||||
$prices_raw = $stmt->fetchAll();
|
||||
|
||||
$prices = [];
|
||||
foreach ($prices_raw as $p) {
|
||||
$prices[$p['item_id']][$p['service_id']] = (float)$p['price'];
|
||||
}
|
||||
|
||||
$items = [];
|
||||
foreach ($items_raw as $i) {
|
||||
$items[$i['id']] = [
|
||||
'id' => $i['id'],
|
||||
'name_en' => $i['name_en'],
|
||||
'name_ar' => $i['name_ar'],
|
||||
'category_id' => $i['category_id'],
|
||||
'image_url' => $i['image_url'],
|
||||
'vat_percent' => (float)$i['vat_percent'],
|
||||
'services' => []
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($all_prices as $p) {
|
||||
if (!isset($items[$p['item_id']])) continue;
|
||||
$item_services = [];
|
||||
foreach ($services_raw as $s) {
|
||||
if (isset($prices[$i['id']][$s['id']])) {
|
||||
$item_services[] = [
|
||||
'id' => $s['id'],
|
||||
'name_en' => $s['name_en'],
|
||||
'name_ar' => $s['name_ar'],
|
||||
'price' => $prices[$i['id']][$s['id']]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$items[$p['item_id']]['services'][] = [
|
||||
'id' => $p['service_id'],
|
||||
'name_en' => $p['service_en'],
|
||||
'name_ar' => $p['service_ar'],
|
||||
'price' => (float)$p['price']
|
||||
];
|
||||
// Only include if has services
|
||||
if (!empty($item_services)) {
|
||||
$items[$i['id']] = [
|
||||
'id' => $i['id'],
|
||||
'name_en' => $i['name_en'],
|
||||
'name_ar' => $i['name_ar'],
|
||||
'category_id' => $i['category_id'],
|
||||
'image_url' => $i['image_url'],
|
||||
'vat_percent' => (float)($i['vat_percent'] ?? 15),
|
||||
'services' => $item_services
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Get all customers for this branch
|
||||
$stmt = db()->prepare("SELECT * FROM customers WHERE branch_id = ? ORDER BY name_en ASC");
|
||||
$stmt->execute([$branch_id]);
|
||||
$customers = $stmt->fetchAll();
|
||||
|
||||
$pageTitle = $lang == 'en' ? 'Point of Sale' : 'نقطة البيع';
|
||||
?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<!-- Category Filter -->
|
||||
<div class="mb-4 overflow-auto d-flex pb-2" style="white-space: nowrap;">
|
||||
<button class="btn btn-primary me-2 cat-filter active" data-cat="all" style="border-radius: 12px; min-width: 80px;">
|
||||
<?= __('all') ?? 'All' ?>
|
||||
</button>
|
||||
<?php foreach($categories as $cat): ?>
|
||||
<button class="btn btn-outline-secondary me-2 cat-filter" data-cat="<?= $cat['id'] ?>" style="border-radius: 12px; min-width: 80px;">
|
||||
<?= $lang === 'ar' ? $cat['name_ar'] : $cat['name_en'] ?>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 mb-4 border-0 shadow-sm" style="border-radius: 25px;">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 class="fw-bold mb-0"><?= __('select_items') ?></h5>
|
||||
<div class="input-group w-50 shadow-sm rounded-4" style="overflow: hidden;">
|
||||
<span class="input-group-text bg-white border-end-0"><i class="bi bi-search"></i></span>
|
||||
<input type="text" id="itemSearch" class="form-control border-start-0 py-2" placeholder="<?= __('search_items') ?>">
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row g-4">
|
||||
<!-- Left: Items & Categories -->
|
||||
<div class="col-lg-8">
|
||||
<div class="mb-4 d-flex align-items-center justify-content-between">
|
||||
<h4 class="fw-bold mb-0"><?= $pageTitle ?></h4>
|
||||
<div class="input-group w-50 shadow-sm rounded-4 overflow-hidden">
|
||||
<span class="input-group-text bg-white border-0"><i class="bi bi-search"></i></span>
|
||||
<input type="text" id="itemSearch" class="form-control border-0 py-2" placeholder="<?= $lang == 'en' ? 'Search items...' : 'بحث عن المنتجات...' ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Categories Scroller -->
|
||||
<div class="mb-4 overflow-auto d-flex pb-2 hide-scrollbar" style="white-space: nowrap;">
|
||||
<button class="btn btn-primary rounded-pill px-4 cat-filter me-2 shadow-sm" data-cat="all">
|
||||
<?= $lang == 'en' ? 'All' : 'الكل' ?>
|
||||
</button>
|
||||
<?php foreach($categories as $cat): ?>
|
||||
<button class="btn btn-white border rounded-pill px-4 cat-filter me-2 shadow-sm" data-cat="<?= $cat['id'] ?>">
|
||||
<?= $lang == 'en' ? $cat['name_en'] : $cat['name_ar'] ?>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="row g-3" id="itemsList">
|
||||
<?php foreach($items as $item): ?>
|
||||
<?php if (empty($item['services'])) continue; ?>
|
||||
<div class="col-md-4 col-sm-6 item-card-wrapper"
|
||||
<div class="col-6 col-md-4 col-xl-3 item-card-wrapper"
|
||||
data-cat="<?= $item['category_id'] ?>"
|
||||
data-name-en="<?= strtolower($item['name_en']) ?>"
|
||||
data-name-ar="<?= $item['name_ar'] ?>">
|
||||
<div class="card h-100 border-0 item-card cursor-pointer shadow-sm overflow-hidden" onclick="showOptions(<?= $item['id'] ?>)">
|
||||
<div class="position-relative">
|
||||
<div class="card h-100 border-0 shadow-sm rounded-4 item-card pointer overflow-hidden transition-all" onclick="showOptions(<?= $item['id'] ?>)">
|
||||
<div class="position-relative bg-light" style="height: 160px; border-radius: 1rem 1rem 0 0; overflow: hidden;">
|
||||
<?php if($item['image_url']): ?>
|
||||
<img src="<?= $item['image_url'] ?>?v=<?= time() ?>" class="card-img-top" style="height: 140px; object-fit: cover;">
|
||||
<img src="<?= $item['image_url'] ?>?v=<?= time() ?>" class="w-100 h-100" style="object-fit: contain; padding: 10px;">
|
||||
<?php else: ?>
|
||||
<div class="bg-light text-center py-4 d-flex align-items-center justify-content-center" style="height: 140px;">
|
||||
<i class="bi bi-bag-check fs-1 text-muted opacity-50"></i>
|
||||
<div class="w-100 h-100 d-flex align-items-center justify-content-center opacity-25">
|
||||
<i class="bi bi-box" style="font-size: 3rem;"></i>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="position-absolute top-0 end-0 m-2">
|
||||
<span class="badge bg-white text-dark shadow-sm rounded-pill px-3 py-2 small"><?= $item['vat_percent'] ?>% VAT</span>
|
||||
<span class="badge bg-white text-dark shadow-sm rounded-pill px-2 py-1 small" style="font-size: 0.65rem;">
|
||||
<?= $item['vat_percent'] ?>% VAT
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-3 text-center">
|
||||
<div class="fw-bold fs-6"><?= $lang === 'ar' ? $item['name_ar'] : $item['name_en'] ?></div>
|
||||
<small class="text-muted d-block mt-1"><?= count($item['services']) ?> <?= __('services') ?></small>
|
||||
<h6 class="card-title mb-1 text-truncate fw-bold small"><?= $lang == 'en' ? $item['name_en'] : $item['name_ar'] ?></h6>
|
||||
<p class="small text-muted mb-0" style="font-size: 0.75rem;"><?= count($item['services']) ?> <?= $lang == 'en' ? 'Services' : 'خدمات' ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card p-4 sticky-top border-0 shadow-sm" style="top: 2rem; max-height: 90vh; overflow-y: auto; border-radius: 25px;">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 class="fw-bold mb-0"><?= __('cart') ?></h5>
|
||||
<button class="btn btn-sm btn-light text-danger rounded-pill px-3" onclick="clearCart()">
|
||||
<i class="bi bi-trash me-1"></i> <?= __('clear') ?? 'Clear' ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label small text-muted fw-bold"><?= __('customer') ?></label>
|
||||
<div class="d-flex">
|
||||
<select id="customerId" class="form-select shadow-sm" style="border-radius: 12px; border: 1px solid #eee;">
|
||||
<option value=""><?= __('walk_in_customer') ?></option>
|
||||
<?php foreach($customers as $c): ?>
|
||||
<option value="<?= $c['id'] ?>"><?= $c['phone'] ?> - <?= $lang === 'ar' ? ($c['name_ar'] ?: $c['name_en']) : $c['name_en'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="btn btn-primary ms-2 shadow-sm" style="border-radius: 12px;" data-bs-toggle="modal" data-bs-target="#addCustomerModal">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
|
||||
<!-- Right: Cart -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 shadow-sm rounded-4 h-100 d-flex flex-column" style="min-height: 80vh;">
|
||||
<div class="card-header bg-white py-3 border-0">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 fw-bold"><?= $lang == 'en' ? 'Current Order' : 'الطلب الحالي' ?></h5>
|
||||
<button class="btn btn-sm btn-outline-danger rounded-3 border-0" onclick="clearCart()">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-3 pb-3">
|
||||
<div class="d-flex gap-2">
|
||||
<!-- Persistent Customer Search -->
|
||||
<div class="position-relative flex-grow-1" id="customerSearchWrapper">
|
||||
<div class="input-group shadow-sm rounded-3 overflow-hidden border">
|
||||
<span class="input-group-text bg-white border-0"><i class="bi bi-person"></i></span>
|
||||
<input type="text" class="form-control border-0 py-2 shadow-none" id="customerSearchInput" placeholder="<?= $lang == 'en' ? 'Walk-in Customer' : 'عميل عابر' ?>" autocomplete="off">
|
||||
<button class="btn btn-white border-0 d-none" type="button" id="clearCustomerBtn">
|
||||
<i class="bi bi-x-circle-fill text-muted"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="position-absolute w-100 shadow-lg bg-white rounded-3 mt-1 d-none" id="customerResults" style="z-index: 1050; max-height: 300px; overflow-y: auto; border: 1px solid #eee;">
|
||||
<div class="p-2 border-bottom">
|
||||
<button class="btn btn-light btn-sm w-100 text-start rounded-2 customer-result-item" data-id="" data-name="<?= $lang == 'en' ? 'Walk-in Customer' : 'عميل عابر' ?>" data-search="walk-in">
|
||||
<?= $lang == 'en' ? 'Walk-in Customer' : 'عميل عابر' ?>
|
||||
</button>
|
||||
</div>
|
||||
<div id="customerResultsList">
|
||||
<?php foreach($customers as $c):
|
||||
$cname = ($lang == 'en' ? $c['name_en'] : $c['name_ar']) ?: $c['name_en'];
|
||||
$searchStr = strtolower($c['name_en'] . ' ' . ($c['name_ar'] ?? '') . ' ' . $c['phone']);
|
||||
?>
|
||||
<div class="p-1">
|
||||
<button class="btn btn-white btn-sm w-100 text-start rounded-2 customer-result-item p-2" type="button" data-id="<?= $c['id'] ?>" data-name="<?= $cname ?>" data-phone="<?= $c['phone'] ?>" data-search="<?= $searchStr ?>">
|
||||
<div class="fw-bold small text-dark"><?= $cname ?></div>
|
||||
<div class="text-muted small" style="font-size: 0.7rem;"><?= $c['phone'] ?></div>
|
||||
</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="customerId" value="">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary rounded-3" data-bs-toggle="modal" data-bs-target="#addCustomerModal">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-0 flex-grow-1 overflow-auto" id="cartContainer">
|
||||
<div id="cartItems" class="p-3"></div>
|
||||
<div id="emptyCart" class="text-center py-5 opacity-50">
|
||||
<i class="bi bi-cart3 display-1 d-block mb-3"></i>
|
||||
<p><?= $lang == 'en' ? 'Your cart is empty' : 'السلة فارغة' ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer bg-white border-top-0 p-3">
|
||||
<div class="bg-light rounded-4 p-3 mb-3">
|
||||
<div class="d-flex justify-content-between mb-2 small text-muted">
|
||||
<span><?= $lang == 'en' ? 'Subtotal' : 'المجموع الفرعي' ?></span>
|
||||
<span id="cartSubtotal">0.00 SAR</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2 small text-muted">
|
||||
<span><?= $lang == 'en' ? 'VAT' : 'الضريبة' ?></span>
|
||||
<span id="cartVat">0.00 SAR</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between pt-2 border-top border-secondary border-opacity-10 mt-2">
|
||||
<h5 class="mb-0 fw-bold"><?= $lang == 'en' ? 'Total' : 'الإجمالي' ?></h5>
|
||||
<h5 id="cartTotal" class="mb-0 fw-bold text-primary">0.00 SAR</h5>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary w-100 py-3 rounded-4 fw-bold shadow-sm" onclick="checkout()">
|
||||
<i class="bi bi-credit-card me-2"></i> <?= $lang == 'en' ? 'Complete Order' : 'إتمام الطلب' ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cartItems" class="mb-4" style="min-height: 200px;">
|
||||
<div class="text-center text-muted mt-5" id="emptyCart">
|
||||
<i class="bi bi-cart-x fs-1 opacity-25"></i>
|
||||
<p class="mt-2"><?= __('cart_is_empty') ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-top pt-4 mt-auto">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-muted"><?= __('subtotal') ?></span>
|
||||
<span class="fw-bold" id="cartSubtotal">0.00 SAR</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<span class="text-muted"><?= __('vat') ?? 'VAT' ?></span>
|
||||
<span class="fw-bold" id="cartVat">0.00 SAR</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-4 p-3 bg-light rounded-4">
|
||||
<span class="fw-bold fs-5"><?= __('total') ?></span>
|
||||
<span class="fw-bold fs-5 text-primary" id="cartTotal">0.00 SAR</span>
|
||||
</div>
|
||||
<button class="btn btn-primary w-100 py-3 fw-bold shadow-sm" style="border-radius: 18px;" onclick="checkout()">
|
||||
<i class="bi bi-shield-check me-2"></i><?= __('checkout') ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selection Modal (Service Options) -->
|
||||
<div class="modal fade" id="selectionModal" tabindex="-1">
|
||||
<!-- Selection Modal -->
|
||||
<div class="modal fade" id="selectionModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow-lg" style="border-radius: 25px;">
|
||||
<div class="modal-header border-0 pb-0 p-4">
|
||||
<div>
|
||||
<h5 class="modal-title fw-bold" id="selectionItemName"></h5>
|
||||
<p class="text-muted small m-0"><?= __('select_service') ?? 'Select service type' ?></p>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
<div class="modal-content border-0 rounded-4 shadow-lg">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<h5 class="modal-title fw-bold" id="selectionItemName"></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4 pt-3">
|
||||
<div class="list-group list-group-flush" id="optionsList">
|
||||
<!-- Service options will be loaded here -->
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<p class="text-muted small mb-3"><?= $lang == 'en' ? 'Select service type:' : 'اختر نوع الخدمة:' ?></p>
|
||||
<div class="row g-2" id="optionsList"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -183,27 +224,31 @@ $customers = $stmt->fetchAll();
|
||||
<!-- Add Customer Modal -->
|
||||
<div class="modal fade" id="addCustomerModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0" style="border-radius: 25px;">
|
||||
<div class="modal-header border-0 p-4 pb-0">
|
||||
<h5 class="modal-title fw-bold"><?= __('add_customer') ?></h5>
|
||||
<div class="modal-content border-0 rounded-4 shadow-lg">
|
||||
<div class="modal-header border-0 pb-0 p-4">
|
||||
<h5 class="modal-title fw-bold"><?= $lang == 'en' ? 'Add New Customer' : 'إضافة عميل جديد' ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<form id="customerForm">
|
||||
<form id="addCustomerForm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold"><?= __('phone') ?> *</label>
|
||||
<input type="text" id="custPhone" class="form-control" required style="border-radius: 12px;">
|
||||
<label class="form-label small"><?= $lang == 'en' ? 'Name (English)' : 'الاسم (إنجليزي)' ?></label>
|
||||
<input type="text" name="name_en" class="form-control rounded-3" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold"><?= __('name_en') ?> *</label>
|
||||
<input type="text" id="custNameEn" class="form-control" required style="border-radius: 12px;">
|
||||
<label class="form-label small"><?= $lang == 'en' ? 'Name (Arabic)' : 'الاسم (عربي)' ?></label>
|
||||
<input type="text" name="name_ar" class="form-control rounded-3">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold"><?= __('name_ar') ?></label>
|
||||
<input type="text" id="custNameAr" class="form-control" style="border-radius: 12px;">
|
||||
<label class="form-label small"><?= $lang == 'en' ? 'Phone Number' : 'رقم الجوال' ?></label>
|
||||
<input type="tel" name="phone" class="form-control rounded-3" required>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary w-100 py-3 fw-bold" style="border-radius: 15px;" onclick="saveCustomer()">
|
||||
<?= __('save') ?>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small"><?= $lang == 'en' ? 'Email (Optional)' : 'البريد (اختياري)' ?></label>
|
||||
<input type="email" name="email" class="form-control rounded-3">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary w-100 py-2 rounded-3" onclick="saveCustomer()">
|
||||
<?= $lang == 'en' ? 'Save Customer' : 'حفظ العميل' ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@ -212,222 +257,328 @@ $customers = $stmt->fetchAll();
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const itemsData = <?= json_encode($items) ?>;
|
||||
const itemsData = <?= json_encode((object)$items) ?>;
|
||||
const lang = '<?= $lang ?>';
|
||||
let cart = JSON.parse(localStorage.getItem('pos_cart') || '[]');
|
||||
let cart = [];
|
||||
let selectionModal;
|
||||
|
||||
const selectionModal = new bootstrap.Modal(document.getElementById('selectionModal'));
|
||||
try {
|
||||
const saved = localStorage.getItem('pos_cart');
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
if (Array.isArray(parsed)) cart = parsed;
|
||||
}
|
||||
} catch (e) { console.error('Cart parse error', e); }
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof bootstrap !== 'undefined') {
|
||||
const modalEl = document.getElementById('selectionModal');
|
||||
if (modalEl) selectionModal = new bootstrap.Modal(modalEl);
|
||||
}
|
||||
|
||||
// Category filtering
|
||||
document.querySelectorAll('.cat-filter').forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
const cat = btn.getAttribute('data-cat');
|
||||
document.querySelectorAll('.cat-filter').forEach(b => b.classList.remove('btn-primary'));
|
||||
document.querySelectorAll('.cat-filter').forEach(b => b.classList.add('btn-white', 'border'));
|
||||
btn.classList.add('btn-primary');
|
||||
btn.classList.remove('btn-white', 'border');
|
||||
|
||||
document.querySelectorAll('.item-card-wrapper').forEach(card => {
|
||||
if (cat === 'all' || card.getAttribute('data-cat') === cat) card.style.display = 'block';
|
||||
else card.style.display = 'none';
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// Search
|
||||
const searchInput = document.getElementById('itemSearch');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
const q = e.target.value.toLowerCase();
|
||||
document.querySelectorAll('.item-card-wrapper').forEach(card => {
|
||||
const en = card.getAttribute('data-name-en') || '';
|
||||
const ar = card.getAttribute('data-name-ar') || '';
|
||||
if (en.includes(q) || ar.includes(q)) card.style.display = 'block';
|
||||
else card.style.display = 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Persistent Customer Search
|
||||
const custSearchInput = document.getElementById('customerSearchInput');
|
||||
const custResults = document.getElementById('customerResults');
|
||||
const clearCustBtn = document.getElementById('clearCustomerBtn');
|
||||
const custIdInput = document.getElementById('customerId');
|
||||
|
||||
if (custSearchInput) {
|
||||
custSearchInput.addEventListener('focus', () => {
|
||||
custResults.classList.remove('d-none');
|
||||
});
|
||||
|
||||
custSearchInput.addEventListener('input', function(e) {
|
||||
const q = e.target.value.toLowerCase();
|
||||
custResults.classList.remove('d-none');
|
||||
document.querySelectorAll('.customer-result-item').forEach(item => {
|
||||
const search = item.getAttribute('data-search') || '';
|
||||
if (search.includes(q)) {
|
||||
item.parentElement.style.display = 'block';
|
||||
} else {
|
||||
item.parentElement.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Close results when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!document.getElementById('customerSearchWrapper').contains(e.target)) {
|
||||
custResults.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (clearCustBtn) {
|
||||
clearCustBtn.addEventListener('click', () => {
|
||||
custIdInput.value = '';
|
||||
custSearchInput.value = '';
|
||||
custSearchInput.placeholder = lang === 'en' ? 'Walk-in Customer' : 'عميل عابر';
|
||||
clearCustBtn.classList.add('d-none');
|
||||
});
|
||||
}
|
||||
|
||||
// Customer selection delegation
|
||||
custResults.addEventListener('click', function(e) {
|
||||
const btn = e.target.closest('.customer-result-item');
|
||||
if (btn) {
|
||||
const id = btn.getAttribute('data-id');
|
||||
const name = btn.getAttribute('data-name');
|
||||
custIdInput.value = id;
|
||||
|
||||
if (id) {
|
||||
custSearchInput.value = name;
|
||||
clearCustBtn.classList.remove('d-none');
|
||||
} else {
|
||||
custSearchInput.value = '';
|
||||
custSearchInput.placeholder = name;
|
||||
clearCustBtn.classList.add('d-none');
|
||||
}
|
||||
|
||||
custResults.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
updateCart();
|
||||
});
|
||||
|
||||
function showOptions(itemId) {
|
||||
const item = itemsData[itemId];
|
||||
document.getElementById('selectionItemName').innerText = lang === 'ar' ? item.name_ar : item.name_en;
|
||||
|
||||
const optionsList = document.getElementById('optionsList');
|
||||
optionsList.innerHTML = '';
|
||||
|
||||
item.services.forEach(service => {
|
||||
addOptionButton(item, service);
|
||||
});
|
||||
|
||||
selectionModal.show();
|
||||
}
|
||||
|
||||
function addOptionButton(item, service) {
|
||||
const optionsList = document.getElementById('optionsList');
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'list-group-item list-group-item-action d-flex justify-content-between align-items-center border-0 py-3 mb-2 rounded-4 bg-light shadow-sm-hover transition';
|
||||
|
||||
let serviceText = lang === 'ar' ? service.name_ar : service.name_en;
|
||||
|
||||
btn.innerHTML = `
|
||||
<div class="fw-bold">${serviceText}</div>
|
||||
<div class="text-primary fw-bold fs-5">${service.price.toFixed(2)} <small style="font-size: 0.8rem;">SAR</small></div>
|
||||
`;
|
||||
btn.onclick = () => addToCart(item, service);
|
||||
optionsList.appendChild(btn);
|
||||
}
|
||||
|
||||
function addToCart(item, service) {
|
||||
const cartItemId = `${item.id}-${service.id}`;
|
||||
const existing = cart.find(i => i.cartItemId === cartItemId);
|
||||
|
||||
const vatPercent = item.vat_percent || 0;
|
||||
const vatAmount = (service.price * (vatPercent / 100));
|
||||
|
||||
if (existing) {
|
||||
existing.quantity++;
|
||||
} else {
|
||||
cart.push({
|
||||
cartItemId,
|
||||
itemId: item.id,
|
||||
serviceId: service.id,
|
||||
itemName: lang === 'ar' ? item.name_ar : item.name_en,
|
||||
serviceName: lang === 'ar' ? service.name_ar : service.name_en,
|
||||
price: service.price,
|
||||
vatPercent: vatPercent,
|
||||
vatAmount: vatAmount,
|
||||
quantity: 1
|
||||
if (!item) return;
|
||||
const itemNameEl = document.getElementById('selectionItemName');
|
||||
if (itemNameEl) itemNameEl.innerText = lang === 'en' ? item.name_en : item.name_ar;
|
||||
const list = document.getElementById('optionsList');
|
||||
if (list) {
|
||||
list.innerHTML = '';
|
||||
item.services.forEach(s => {
|
||||
const col = document.createElement('div');
|
||||
col.className = 'col-6';
|
||||
col.innerHTML = `
|
||||
<button class="btn btn-outline-primary w-100 p-3 rounded-4 border-2 text-center h-100 transition-all" onclick="addToCart(${item.id}, ${s.id})">
|
||||
<div class="fw-bold mb-1 small">${lang === 'en' ? s.name_en : s.name_ar}</div>
|
||||
<div class="small opacity-75">${s.price.toFixed(2)} SAR</div>
|
||||
</button>
|
||||
`;
|
||||
list.appendChild(col);
|
||||
});
|
||||
}
|
||||
|
||||
if (selectionModal) selectionModal.show();
|
||||
}
|
||||
|
||||
function addToCart(itemId, serviceId) {
|
||||
const item = itemsData[itemId];
|
||||
if (!item) return;
|
||||
const service = item.services.find(s => s.id === serviceId);
|
||||
if (!service) return;
|
||||
|
||||
const existing = cart.find(i => i.item_id === itemId && i.service_id === serviceId);
|
||||
if (existing) {
|
||||
existing.qty++;
|
||||
} else {
|
||||
cart.push({
|
||||
item_id: itemId,
|
||||
service_id: serviceId,
|
||||
name: lang === 'en' ? item.name_en : item.name_ar,
|
||||
service_name: lang === 'en' ? service.name_en : service.name_ar,
|
||||
price: service.price,
|
||||
qty: 1,
|
||||
vat_percent: item.vat_percent
|
||||
});
|
||||
}
|
||||
if (selectionModal) selectionModal.hide();
|
||||
updateCart();
|
||||
}
|
||||
|
||||
function changeQty(index, delta) {
|
||||
cart[index].qty += delta;
|
||||
if (cart[index].qty <= 0) cart.splice(index, 1);
|
||||
updateCart();
|
||||
selectionModal.hide();
|
||||
}
|
||||
|
||||
function updateCart() {
|
||||
localStorage.setItem('pos_cart', JSON.stringify(cart));
|
||||
const cartList = document.getElementById('cartItems');
|
||||
const emptyCart = document.getElementById('emptyCart');
|
||||
|
||||
cartList.querySelectorAll('.cart-item-row').forEach(e => e.remove());
|
||||
if (!cartList || !emptyCart) return;
|
||||
|
||||
if (cart.length === 0) {
|
||||
cartList.innerHTML = '';
|
||||
emptyCart.style.display = 'block';
|
||||
} else {
|
||||
emptyCart.style.display = 'none';
|
||||
cart.forEach((item, index) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'cart-item-row d-flex justify-content-between align-items-center mb-3 p-3 bg-white border rounded-4 shadow-sm';
|
||||
row.innerHTML = `
|
||||
<div style="flex: 1;">
|
||||
<div class="fw-bold small">${item.itemName}</div>
|
||||
<div class="text-muted" style="font-size: 0.75rem;">${item.serviceName}</div>
|
||||
cartList.innerHTML = cart.map((item, index) => `
|
||||
<div class="d-flex align-items-center mb-3 bg-light p-2 rounded-3">
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-bold small text-dark">${item.name}</div>
|
||||
<div class="text-muted" style="font-size: 0.75rem;">${item.service_name}</div>
|
||||
<div class="fw-bold text-primary">${item.price.toFixed(2)} SAR</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mx-3 bg-light rounded-pill px-2 py-1">
|
||||
<button class="btn btn-sm p-0 text-muted" onclick="changeQty(${index}, -1)"><i class="bi bi-dash-circle-fill"></i></button>
|
||||
<span class="mx-3 fw-bold" style="min-width: 15px; text-align: center;">${item.quantity}</span>
|
||||
<button class="btn btn-sm p-0 text-primary" onclick="changeQty(${index}, 1)"><i class="bi bi-plus-circle-fill"></i></button>
|
||||
<div class="d-flex align-items-center bg-white rounded-3 p-1">
|
||||
<button class="btn btn-sm p-0 px-2" onclick="changeQty(${index}, -1)"><i class="bi bi-dash"></i></button>
|
||||
<span class="mx-2 fw-bold">${item.qty}</span>
|
||||
<button class="btn btn-sm p-0 px-2" onclick="changeQty(${index}, 1)"><i class="bi bi-plus"></i></button>
|
||||
</div>
|
||||
<div class="fw-bold text-end" style="width: 70px;">
|
||||
${(item.price * item.quantity).toFixed(2)}
|
||||
</div>
|
||||
`;
|
||||
cartList.appendChild(row);
|
||||
});
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
const subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
const vat = cart.reduce((sum, item) => sum + (item.vatAmount * item.quantity), 0);
|
||||
const total = subtotal + vat;
|
||||
let subtotal = 0;
|
||||
let totalVat = 0;
|
||||
|
||||
document.getElementById('cartSubtotal').innerText = subtotal.toFixed(2) + ' SAR';
|
||||
document.getElementById('cartVat').innerText = vat.toFixed(2) + ' SAR';
|
||||
document.getElementById('cartTotal').innerText = total.toFixed(2) + ' SAR';
|
||||
}
|
||||
cart.forEach(item => {
|
||||
const itemSubtotal = item.price * item.qty;
|
||||
subtotal += itemSubtotal;
|
||||
totalVat += itemSubtotal * ((item.vat_percent || 15) / 100);
|
||||
});
|
||||
|
||||
function changeQty(index, delta) {
|
||||
cart[index].quantity += delta;
|
||||
if (cart[index].quantity <= 0) {
|
||||
cart.splice(index, 1);
|
||||
}
|
||||
updateCart();
|
||||
const subtotalEl = document.getElementById('cartSubtotal');
|
||||
const vatEl = document.getElementById('cartVat');
|
||||
const totalEl = document.getElementById('cartTotal');
|
||||
|
||||
if (subtotalEl) subtotalEl.innerText = subtotal.toFixed(2) + ' SAR';
|
||||
if (vatEl) vatEl.innerText = totalVat.toFixed(2) + ' SAR';
|
||||
if (totalEl) totalEl.innerText = (subtotal + totalVat).toFixed(2) + ' SAR';
|
||||
}
|
||||
|
||||
function clearCart() {
|
||||
if (confirm('Are you sure you want to clear the cart?')) {
|
||||
if (confirm(lang === 'en' ? 'Clear cart?' : 'مسح السلة؟')) {
|
||||
cart = [];
|
||||
updateCart();
|
||||
}
|
||||
}
|
||||
|
||||
function checkout() {
|
||||
if (cart.length === 0) { alert('Cart is empty'); return; }
|
||||
const customerId = document.getElementById('customerId').value;
|
||||
const subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
const vat = cart.reduce((sum, item) => sum + (item.vatAmount * item.quantity), 0);
|
||||
async function checkout() {
|
||||
if (cart.length === 0) return;
|
||||
const cid = document.getElementById('customerId').value;
|
||||
|
||||
fetch('api/checkout.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
customer_id: customerId,
|
||||
items: cart,
|
||||
vat_total: vat,
|
||||
total_price: subtotal + vat
|
||||
})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
let subtotal = 0;
|
||||
let totalVat = 0;
|
||||
|
||||
// Map cart items to the keys expected by checkout.php
|
||||
const itemsToSubmit = cart.map(item => {
|
||||
const itemSubtotal = item.price * item.qty;
|
||||
const itemVat = itemSubtotal * ((item.vat_percent || 15) / 100);
|
||||
|
||||
subtotal += itemSubtotal;
|
||||
totalVat += itemVat;
|
||||
|
||||
return {
|
||||
itemId: item.item_id,
|
||||
serviceId: item.service_id,
|
||||
variantId: null,
|
||||
quantity: item.qty,
|
||||
price: item.price,
|
||||
vatAmount: itemVat
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch('api/checkout.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
customer_id: cid,
|
||||
items: itemsToSubmit,
|
||||
vat_total: totalVat,
|
||||
total_price: subtotal + totalVat
|
||||
})
|
||||
});
|
||||
const res = await response.json();
|
||||
if (res.success) {
|
||||
cart = [];
|
||||
localStorage.removeItem('pos_cart');
|
||||
updateCart();
|
||||
window.location.href = 'order_details.php?id=' + res.order_id;
|
||||
} else alert('Error: ' + res.error);
|
||||
});
|
||||
} else alert(res.error);
|
||||
} catch (e) { alert('Error'); }
|
||||
}
|
||||
|
||||
function saveCustomer() {
|
||||
const phone = document.getElementById('custPhone').value;
|
||||
const name_en = document.getElementById('custNameEn').value;
|
||||
const name_ar = document.getElementById('custNameAr').value;
|
||||
|
||||
if (!phone || !name_en) { alert('Please fill required fields'); return; }
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('phone', phone);
|
||||
formData.append('name_en', name_en);
|
||||
formData.append('name_ar', name_ar);
|
||||
|
||||
fetch('api/add_customer.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
async function saveCustomer() {
|
||||
const form = document.getElementById('addCustomerForm');
|
||||
if (!form) return;
|
||||
const data = new FormData(form);
|
||||
try {
|
||||
const response = await fetch('api/add_customer.php', { method: 'POST', body: data });
|
||||
const res = await response.json();
|
||||
if (res.success) {
|
||||
const select = document.getElementById('customerId');
|
||||
const opt = document.createElement('option');
|
||||
opt.value = res.id;
|
||||
opt.text = phone + ' - ' + (lang === 'ar' ? (name_ar || name_en) : name_en);
|
||||
opt.selected = true;
|
||||
select.add(opt);
|
||||
bootstrap.Modal.getInstance(document.getElementById('addCustomerModal')).hide();
|
||||
} else alert('Error: ' + res.error);
|
||||
});
|
||||
const id = res.customer.id;
|
||||
const nameEn = res.customer.name_en;
|
||||
const nameAr = res.customer.name_ar || nameEn;
|
||||
const phone = res.customer.phone;
|
||||
const displayName = lang === 'en' ? nameEn : nameAr;
|
||||
|
||||
// Update hidden input
|
||||
document.getElementById('customerId').value = id;
|
||||
// Update search input
|
||||
const custSearchInput = document.getElementById('customerSearchInput');
|
||||
custSearchInput.value = displayName;
|
||||
document.getElementById('clearCustomerBtn').classList.remove('d-none');
|
||||
|
||||
// Add to results list
|
||||
const list = document.getElementById('customerResultsList');
|
||||
if (list) {
|
||||
const searchStr = `${nameEn} ${nameAr} ${phone}`.toLowerCase();
|
||||
const div = document.createElement('div');
|
||||
div.className = 'p-1';
|
||||
div.innerHTML = `
|
||||
<button class="btn btn-white btn-sm w-100 text-start rounded-2 customer-result-item p-2" type="button" data-id="${id}" data-name="${displayName}" data-phone="${phone}" data-search="${searchStr}">
|
||||
<div class="fw-bold small text-dark">${displayName}</div>
|
||||
<div class="text-muted small" style="font-size: 0.7rem;">${phone}</div>
|
||||
</button>
|
||||
`;
|
||||
list.prepend(div);
|
||||
}
|
||||
|
||||
if (typeof bootstrap !== 'undefined') {
|
||||
const modalEl = document.getElementById('addCustomerModal');
|
||||
if (modalEl) {
|
||||
const modal = bootstrap.Modal.getInstance(modalEl);
|
||||
if (modal) modal.hide();
|
||||
}
|
||||
}
|
||||
form.reset();
|
||||
} else alert(res.error);
|
||||
} catch (e) { alert('Error'); }
|
||||
}
|
||||
|
||||
// Category filter
|
||||
document.querySelectorAll('.cat-filter').forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
document.querySelectorAll('.cat-filter').forEach(b => b.classList.remove('active', 'btn-primary'));
|
||||
document.querySelectorAll('.cat-filter').forEach(b => b.classList.add('btn-outline-secondary'));
|
||||
btn.classList.add('active', 'btn-primary');
|
||||
btn.classList.remove('btn-outline-secondary');
|
||||
|
||||
const cat = btn.getAttribute('data-cat');
|
||||
document.querySelectorAll('.item-card-wrapper').forEach(card => {
|
||||
if (cat === 'all' || card.getAttribute('data-cat') === cat) card.style.display = 'block';
|
||||
else card.style.display = 'none';
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// Item search
|
||||
document.getElementById('itemSearch').addEventListener('input', function(e) {
|
||||
const q = e.target.value.toLowerCase();
|
||||
document.querySelectorAll('.item-card-wrapper').forEach(card => {
|
||||
const en = card.getAttribute('data-name-en');
|
||||
const ar = card.getAttribute('data-name-ar');
|
||||
if (en.includes(q) || ar.includes(q)) card.style.display = 'block';
|
||||
else card.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize cart on load
|
||||
updateCart();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.cursor-pointer { cursor: pointer; }
|
||||
.item-card { transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); border-radius: 25px; border: 1px solid transparent; }
|
||||
.item-card:hover { transform: translateY(-8px); border-color: var(--bs-primary) !important; box-shadow: 0 15px 30px rgba(13, 110, 253, 0.15) !important; }
|
||||
.bg-light { background-color: #f8f9fa !important; }
|
||||
.rounded-4 { border-radius: 1rem !important; }
|
||||
.cat-filter.active { color: white !important; }
|
||||
.bg-soft-primary { background-color: rgba(13, 110, 253, 0.1); }
|
||||
.list-group-item-action:hover { background-color: #e9ecef !important; transform: scale(1.02); transition: all 0.2s; }
|
||||
.transition { transition: all 0.2s; }
|
||||
.shadow-sm-hover:hover { shadow: 0 .125rem .25rem rgba(0,0,0,.075)!important; }
|
||||
.pointer { cursor: pointer; }
|
||||
.item-card:hover { transform: translateY(-5px); }
|
||||
.transition-all { transition: all 0.3s ease; }
|
||||
.hide-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
.btn-white { background: white; }
|
||||
.customer-result-item:hover { background-color: #f8f9fa; }
|
||||
</style>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user