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;
}
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') {
header('Content-Type: application/json');
$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();
$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 = [];
foreach ($products_raw as $p) {
@ -6228,31 +6290,48 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
}
};
document.querySelectorAll('.product-card').forEach(card => {
card.addEventListener('click', () => {
const product = {
id: parseInt(card.dataset.id),
nameEn: card.dataset.nameEn,
nameAr: card.dataset.nameAr,
price: parseFloat(card.dataset.price),
stock_quantity: parseFloat(card.dataset.stockQuantity),
vatRate: parseFloat(card.dataset.vatRate) || 0
};
cart.add(product);
});
// Event Delegation for clicking on cards
document.getElementById('productGrid').addEventListener('click', (e) => {
const card = e.target.closest('.product-card');
if (card) {
addToCartFromCard(card);
}
});
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) => {
const q = e.target.value.toLowerCase();
document.querySelectorAll('.product-grid .product-card').forEach(card => {
const name = card.dataset.nameEn.toLowerCase() + ' ' + card.dataset.nameAr.toLowerCase();
const sku = card.dataset.sku.toLowerCase();
if (name.includes(q) || sku.includes(q)) {
card.style.display = 'flex';
} else {
card.style.display = 'none';
}
});
const q = e.target.value.trim();
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const grid = document.getElementById('productGrid');
grid.style.opacity = '0.5';
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) => {
@ -6260,41 +6339,38 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
const barcode = e.target.value.trim();
if (!barcode) return;
// Try finding in DOM first (current view)
const card = Array.from(document.querySelectorAll('.product-card')).find(c => c.dataset.sku === barcode);
if (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);
addToCartFromCard(card);
e.target.value = '';
Swal.fire({
toast: true,
position: 'top-end',
icon: 'success',
title: 'Added: ' + product.nameEn,
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();
// Not found in current view, check server
fetch('index.php?action=pos_get_item_by_sku&sku=' + encodeURIComponent(barcode))
.then(response => response.json())
.then(product => {
if (product) {
cart.add(product);
e.target.value = '';
Swal.fire({
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
document.addEventListener('click', () => {
if (document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'SELECT' && document.activeElement.tagName !== 'TEXTAREA') {