feat: Limit POS items grid to 100 initially and add dynamic server-side search
This commit is contained in:
parent
71282f7fc3
commit
443ae2521f
176
index.php
176
index.php
@ -453,6 +453,68 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if ($action === 'pos_search_items') {
|
||||||
|
$q = $_GET['q'] ?? '';
|
||||||
|
$searchTerm = "%$q%";
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM stock_items WHERE (name_en LIKE ? OR name_ar LIKE ? OR sku LIKE ?) ORDER BY name_en ASC LIMIT 100";
|
||||||
|
$products_raw = db()->prepare($sql);
|
||||||
|
$products_raw->execute([$searchTerm, $searchTerm, $searchTerm]);
|
||||||
|
while($p = $products_raw->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
$p['original_price'] = (float)$p['sale_price'];
|
||||||
|
$p['sale_price'] = getPromotionalPrice($p);
|
||||||
|
|
||||||
|
// Render Card HTML
|
||||||
|
?>
|
||||||
|
<div class="product-card" data-id="<?= $p['id'] ?>" data-name-en="<?= htmlspecialchars($p['name_en']) ?>" data-name-ar="<?= htmlspecialchars($p['name_ar']) ?>" data-price="<?= $p['sale_price'] ?>" data-sku="<?= htmlspecialchars($p['sku']) ?>" data-stock-quantity="<?= (float)$p['stock_quantity'] ?>" data-vat-rate="<?= $p['vat_rate'] ?>">
|
||||||
|
<?php if ($p['image_path']): ?>
|
||||||
|
<img src="<?= htmlspecialchars($p['image_path']) ?>" alt="<?= htmlspecialchars($p['name_en']) ?>">
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="bg-light d-flex align-items-center justify-content-center rounded mb-2" style="height: 120px;">
|
||||||
|
<i class="bi bi-box-seam text-muted" style="font-size: 3rem;"></i>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="fw-bold mb-1 product-name" data-en="<?= htmlspecialchars($p['name_en']) ?>" data-ar="<?= htmlspecialchars($p['name_ar']) ?>"><?= htmlspecialchars($p['name_en']) ?></div>
|
||||||
|
<div class="small text-muted mb-2"><?= htmlspecialchars($p['sku']) ?></div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mt-auto">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<?php if ($p['sale_price'] < $p['original_price']): ?>
|
||||||
|
<span class="text-muted smaller text-decoration-line-through">OMR <?= number_format($p['original_price'], 3) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<span class="price text-primary fw-bold">OMR <?= number_format((float)$p['sale_price'], 3) ?></span>
|
||||||
|
</div>
|
||||||
|
<span class="badge bg-light text-dark small"><?= (float)$p['stock_quantity'] ?> left</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'pos_get_item_by_sku') {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$sku = $_GET['sku'] ?? '';
|
||||||
|
if (!$sku) { echo json_encode(null); exit; }
|
||||||
|
|
||||||
|
$stmt = db()->prepare("SELECT * FROM stock_items WHERE sku = ? LIMIT 1");
|
||||||
|
$stmt->execute([$sku]);
|
||||||
|
$p = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($p) {
|
||||||
|
$p['original_price'] = (float)$p['sale_price'];
|
||||||
|
$p['sale_price'] = getPromotionalPrice($p);
|
||||||
|
$p['nameEn'] = $p['name_en'];
|
||||||
|
$p['nameAr'] = $p['name_ar'];
|
||||||
|
$p['vatRate'] = $p['vat_rate'];
|
||||||
|
echo json_encode($p);
|
||||||
|
} else {
|
||||||
|
echo json_encode(null);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if ($action === 'get_payments') {
|
if ($action === 'get_payments') {
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
$invoice_id = (int)$_GET['invoice_id'];
|
$invoice_id = (int)$_GET['invoice_id'];
|
||||||
@ -5310,7 +5372,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
|||||||
$registers = db()->query("SELECT * FROM cash_registers WHERE status = 'active'")->fetchAll();
|
$registers = db()->query("SELECT * FROM cash_registers WHERE status = 'active'")->fetchAll();
|
||||||
|
|
||||||
$allow_zero_stock_sell = ($data['settings']['allow_zero_stock_sell'] ?? '1') === '1';
|
$allow_zero_stock_sell = ($data['settings']['allow_zero_stock_sell'] ?? '1') === '1';
|
||||||
$sql = "SELECT * FROM stock_items ORDER BY name_en ASC";
|
$sql = "SELECT * FROM stock_items ORDER BY name_en ASC LIMIT 100";
|
||||||
$products_raw = db()->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
$products_raw = db()->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
||||||
$products = [];
|
$products = [];
|
||||||
foreach ($products_raw as $p) {
|
foreach ($products_raw as $p) {
|
||||||
@ -6228,31 +6290,48 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.querySelectorAll('.product-card').forEach(card => {
|
|
||||||
card.addEventListener('click', () => {
|
// Event Delegation for clicking on cards
|
||||||
const product = {
|
document.getElementById('productGrid').addEventListener('click', (e) => {
|
||||||
id: parseInt(card.dataset.id),
|
const card = e.target.closest('.product-card');
|
||||||
nameEn: card.dataset.nameEn,
|
if (card) {
|
||||||
nameAr: card.dataset.nameAr,
|
addToCartFromCard(card);
|
||||||
price: parseFloat(card.dataset.price),
|
}
|
||||||
stock_quantity: parseFloat(card.dataset.stockQuantity),
|
|
||||||
vatRate: parseFloat(card.dataset.vatRate) || 0
|
|
||||||
};
|
|
||||||
cart.add(product);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function addToCartFromCard(card) {
|
||||||
|
const product = {
|
||||||
|
id: parseInt(card.dataset.id),
|
||||||
|
nameEn: card.dataset.nameEn,
|
||||||
|
nameAr: card.dataset.nameAr,
|
||||||
|
price: parseFloat(card.dataset.price),
|
||||||
|
sku: card.dataset.sku,
|
||||||
|
stock_quantity: parseFloat(card.dataset.stockQuantity),
|
||||||
|
vatRate: parseFloat(card.dataset.vatRate) || 0
|
||||||
|
};
|
||||||
|
cart.add(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchTimeout;
|
||||||
document.getElementById('productSearch').addEventListener('input', (e) => {
|
document.getElementById('productSearch').addEventListener('input', (e) => {
|
||||||
const q = e.target.value.toLowerCase();
|
const q = e.target.value.trim();
|
||||||
document.querySelectorAll('.product-grid .product-card').forEach(card => {
|
clearTimeout(searchTimeout);
|
||||||
const name = card.dataset.nameEn.toLowerCase() + ' ' + card.dataset.nameAr.toLowerCase();
|
|
||||||
const sku = card.dataset.sku.toLowerCase();
|
searchTimeout = setTimeout(() => {
|
||||||
if (name.includes(q) || sku.includes(q)) {
|
const grid = document.getElementById('productGrid');
|
||||||
card.style.display = 'flex';
|
grid.style.opacity = '0.5';
|
||||||
} else {
|
|
||||||
card.style.display = 'none';
|
fetch('index.php?action=pos_search_items&q=' + encodeURIComponent(q))
|
||||||
}
|
.then(response => response.text())
|
||||||
});
|
.then(html => {
|
||||||
|
grid.innerHTML = html;
|
||||||
|
grid.style.opacity = '1';
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
grid.style.opacity = '1';
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('barcodeInput').addEventListener('keypress', (e) => {
|
document.getElementById('barcodeInput').addEventListener('keypress', (e) => {
|
||||||
@ -6260,41 +6339,38 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
|||||||
const barcode = e.target.value.trim();
|
const barcode = e.target.value.trim();
|
||||||
if (!barcode) return;
|
if (!barcode) return;
|
||||||
|
|
||||||
|
// Try finding in DOM first (current view)
|
||||||
const card = Array.from(document.querySelectorAll('.product-card')).find(c => c.dataset.sku === barcode);
|
const card = Array.from(document.querySelectorAll('.product-card')).find(c => c.dataset.sku === barcode);
|
||||||
|
|
||||||
if (card) {
|
if (card) {
|
||||||
const product = {
|
addToCartFromCard(card);
|
||||||
id: parseInt(card.dataset.id),
|
|
||||||
nameEn: card.dataset.nameEn,
|
|
||||||
nameAr: card.dataset.nameAr,
|
|
||||||
price: parseFloat(card.dataset.price),
|
|
||||||
sku: card.dataset.sku,
|
|
||||||
stock_quantity: parseFloat(card.dataset.stockQuantity),
|
|
||||||
vatRate: parseFloat(card.dataset.vatRate) || 0
|
|
||||||
};
|
|
||||||
cart.add(product);
|
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
Swal.fire({
|
|
||||||
toast: true,
|
|
||||||
position: 'top-end',
|
|
||||||
icon: 'success',
|
|
||||||
title: 'Added: ' + product.nameEn,
|
|
||||||
showConfirmButton: false,
|
|
||||||
timer: 1000
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
Swal.fire({
|
// Not found in current view, check server
|
||||||
toast: true,
|
fetch('index.php?action=pos_get_item_by_sku&sku=' + encodeURIComponent(barcode))
|
||||||
position: 'top-end',
|
.then(response => response.json())
|
||||||
icon: 'error',
|
.then(product => {
|
||||||
title: 'Product not found',
|
if (product) {
|
||||||
showConfirmButton: false,
|
cart.add(product);
|
||||||
timer: 1500
|
e.target.value = '';
|
||||||
});
|
Swal.fire({
|
||||||
e.target.select();
|
toast: true, position: 'top-end', icon: 'success',
|
||||||
|
title: (document.documentElement.lang === 'ar' ? 'تم إضافة: ' : 'Added: ') + (document.documentElement.lang === 'ar' ? (product.nameAr || product.nameEn) : (product.nameEn || product.nameAr)),
|
||||||
|
showConfirmButton: false, timer: 1000
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Swal.fire({
|
||||||
|
toast: true, position: 'top-end', icon: 'error',
|
||||||
|
title: 'Product not found', showConfirmButton: false, timer: 1500
|
||||||
|
});
|
||||||
|
e.target.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Keep barcode input focused
|
// Keep barcode input focused
|
||||||
document.addEventListener('click', () => {
|
document.addEventListener('click', () => {
|
||||||
if (document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'SELECT' && document.activeElement.tagName !== 'TEXTAREA') {
|
if (document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'SELECT' && document.activeElement.tagName !== 'TEXTAREA') {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user