feat: Limit POS items grid to 100 initially and add dynamic server-side search

This commit is contained in:
Flatlogic Bot 2026-03-19 14:18:18 +00:00
parent 71282f7fc3
commit 443ae2521f

176
index.php
View File

@ -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') {