364 lines
17 KiB
JavaScript
364 lines
17 KiB
JavaScript
'''
|
|
// Main javascript file for Opulent POS
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const page = document.body.dataset.page;
|
|
|
|
// --- Logic for Cashier Checkout Page ---
|
|
if (page === 'cashier_checkout') {
|
|
// --- Element Selectors ---
|
|
const barcodeInput = document.getElementById('barcode-scanner-input');
|
|
const productSearchInput = document.getElementById('product-search');
|
|
const productGrid = document.getElementById('product-grid');
|
|
const productGridPlaceholder = document.getElementById('product-grid-placeholder');
|
|
const cartItemsContainer = document.getElementById('cart-items');
|
|
const cartPlaceholder = document.getElementById('cart-placeholder');
|
|
const cartItemCount = document.getElementById('cart-item-count');
|
|
const cartSubtotal = document.getElementById('cart-subtotal');
|
|
const cartTax = document.getElementById('cart-tax');
|
|
const cartTotal = document.getElementById('cart-total');
|
|
const completeSaleBtn = document.getElementById('complete-sale-btn');
|
|
const cancelSaleBtn = document.getElementById('cancel-sale-btn');
|
|
const printLastInvoiceBtn = document.getElementById('print-last-invoice-btn');
|
|
|
|
// --- State Management ---
|
|
let cart = JSON.parse(localStorage.getItem('cart')) || {};
|
|
|
|
// --- Utility Functions ---
|
|
const debounce = (func, delay) => {
|
|
let timeout;
|
|
return function(...args) {
|
|
const context = this;
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(() => func.apply(context, args), delay);
|
|
};
|
|
};
|
|
|
|
const formatCurrency = (amount) => `PKR ${parseFloat(amount).toFixed(2)}`;
|
|
|
|
// --- API Communication ---
|
|
const searchProducts = async (query) => {
|
|
if (query.length < 2) {
|
|
productGrid.innerHTML = '';
|
|
productGridPlaceholder.style.display = 'block';
|
|
return;
|
|
}
|
|
try {
|
|
const response = await fetch(`api/search_products.php?q=${encodeURIComponent(query)}`);
|
|
if (!response.ok) throw new Error('Network response was not ok');
|
|
const products = await response.json();
|
|
renderProductGrid(products);
|
|
} catch (error) {
|
|
console.error('Error fetching products:', error);
|
|
productGrid.innerHTML = '<p class="text-danger col-12">Could not fetch products.</p>';
|
|
}
|
|
};
|
|
|
|
const findProductByBarcode = async (barcode) => {
|
|
try {
|
|
const response = await fetch(`api/search_products.php?q=${encodeURIComponent(barcode)}&exact=true`);
|
|
if (!response.ok) throw new Error('Network response was not ok');
|
|
const products = await response.json();
|
|
if (products.length > 0) {
|
|
addToCart(products[0]);
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (error) {
|
|
console.error('Error fetching product by barcode:', error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// --- Rendering Functions ---
|
|
const renderProductGrid = (products) => {
|
|
productGrid.innerHTML = '';
|
|
if (products.length === 0) {
|
|
productGridPlaceholder.innerHTML = '<p>No products found.</p>';
|
|
productGridPlaceholder.style.display = 'block';
|
|
return;
|
|
}
|
|
productGridPlaceholder.style.display = 'none';
|
|
products.forEach(product => {
|
|
const productCard = document.createElement('div');
|
|
productCard.className = 'col';
|
|
productCard.innerHTML = `
|
|
<div class="card h-100 product-card" role="button" data-product-id="${product.id}" data-product-name="${product.name}" data-product-price="${product.price}" data-product-barcode="${product.barcode}">
|
|
<div class="card-body text-center">
|
|
<h6 class="card-title fs-sm">${product.name}</h6>
|
|
<p class="card-text text-muted small">${formatCurrency(product.price)}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
productGrid.appendChild(productCard);
|
|
});
|
|
};
|
|
|
|
const renderCart = () => {
|
|
cartItemsContainer.innerHTML = '';
|
|
let subtotal = 0;
|
|
let itemCount = 0;
|
|
|
|
if (Object.keys(cart).length === 0) {
|
|
cartItemsContainer.appendChild(cartPlaceholder);
|
|
} else {
|
|
const table = document.createElement('table');
|
|
table.className = 'table table-sm';
|
|
table.innerHTML = `
|
|
<thead>
|
|
<tr>
|
|
<th>Item</th>
|
|
<th class="text-center">Qty</th>
|
|
<th class="text-end">Price</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>`;
|
|
const tbody = table.querySelector('tbody');
|
|
|
|
for (const productId in cart) {
|
|
const item = cart[productId];
|
|
subtotal += item.price * item.quantity;
|
|
itemCount += item.quantity;
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<td>
|
|
<div class="fs-sm">${item.name}</div>
|
|
<small class="text-muted">${formatCurrency(item.price)}</small>
|
|
</td>
|
|
<td class="text-center">
|
|
<div class="input-group input-group-sm" style="width: 90px; margin: auto;">
|
|
<button class="btn btn-outline-secondary btn-sm cart-quantity-change" data-product-id="${productId}" data-change="-1">-</button>
|
|
<input type="text" class="form-control text-center" value="${item.quantity}" readonly>
|
|
<button class="btn btn-outline-secondary btn-sm cart-quantity-change" data-product-id="${productId}" data-change="1">+</button>
|
|
</div>
|
|
</td>
|
|
<td class="text-end fw-bold">${formatCurrency(item.price * item.quantity)}</td>
|
|
<td class="text-center">
|
|
<button class="btn btn-sm btn-outline-danger cart-remove-item" data-product-id="${productId}"><i class="bi bi-trash"></i></button>
|
|
</td>
|
|
`;
|
|
tbody.appendChild(row);
|
|
}
|
|
cartItemsContainer.appendChild(table);
|
|
}
|
|
|
|
const tax = subtotal * 0;
|
|
const total = subtotal + tax;
|
|
cartSubtotal.textContent = formatCurrency(subtotal);
|
|
cartTax.textContent = formatCurrency(tax);
|
|
cartTotal.textContent = formatCurrency(total);
|
|
cartItemCount.textContent = itemCount;
|
|
completeSaleBtn.disabled = itemCount === 0;
|
|
};
|
|
|
|
// --- Cart Logic ---
|
|
const saveCart = () => localStorage.setItem('cart', JSON.stringify(cart));
|
|
const addToCart = (product) => {
|
|
const id = product.id;
|
|
if (cart[id]) {
|
|
cart[id].quantity++;
|
|
} else {
|
|
cart[id] = { id: id, name: product.name, price: parseFloat(product.price), quantity: 1, barcode: product.barcode };
|
|
}
|
|
saveCart();
|
|
renderCart();
|
|
};
|
|
const updateCartQuantity = (productId, change) => {
|
|
if (cart[productId]) {
|
|
cart[productId].quantity += change;
|
|
if (cart[productId].quantity <= 0) delete cart[productId];
|
|
saveCart();
|
|
renderCart();
|
|
}
|
|
};
|
|
const removeFromCart = (productId) => {
|
|
if (cart[productId]) {
|
|
delete cart[productId];
|
|
saveCart();
|
|
renderCart();
|
|
}
|
|
};
|
|
const clearCart = () => { cart = {}; saveCart(); renderCart(); };
|
|
|
|
// --- Sale Completion & Invoicing ---
|
|
const completeSale = async () => {
|
|
if (Object.keys(cart).length === 0) return alert('Cannot complete sale with an empty cart.');
|
|
if (!confirm('Are you sure you want to complete this sale?')) return;
|
|
|
|
completeSaleBtn.disabled = true;
|
|
completeSaleBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Processing...';
|
|
try {
|
|
const response = await fetch('api/complete_sale.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cart) });
|
|
const result = await response.json();
|
|
if (response.ok && result.success) {
|
|
alert(`Sale Completed Successfully!
|
|
Receipt Number: ${result.receipt_number}`);
|
|
localStorage.setItem('lastSale', JSON.stringify({ cart: { ...cart }, ...result }));
|
|
clearCart();
|
|
} else {
|
|
throw new Error(result.error || 'An unknown error occurred.');
|
|
}
|
|
} catch (error) {
|
|
alert(`Failed to complete sale: ${error.message}`);
|
|
} finally {
|
|
completeSaleBtn.disabled = false;
|
|
completeSaleBtn.innerHTML = 'Complete Sale';
|
|
}
|
|
};
|
|
|
|
const printInvoice = () => {
|
|
const lastSale = JSON.parse(localStorage.getItem('lastSale'));
|
|
if (!lastSale) {
|
|
alert('No last sale found to print.');
|
|
return;
|
|
}
|
|
|
|
// Populate invoice details
|
|
document.getElementById('invoice-receipt-number').textContent = lastSale.receipt_number;
|
|
document.getElementById('invoice-cashier-name').textContent = lastSale.cashier_name || 'N/A';
|
|
document.getElementById('invoice-date').textContent = new Date(lastSale.created_at).toLocaleString();
|
|
|
|
const itemsTable = document.getElementById('invoice-items-table');
|
|
itemsTable.innerHTML = '';
|
|
let subtotal = 0;
|
|
let i = 0;
|
|
for (const productId in lastSale.cart) {
|
|
const item = lastSale.cart[productId];
|
|
const total = item.price * item.quantity;
|
|
subtotal += total;
|
|
const row = itemsTable.insertRow();
|
|
row.innerHTML = `
|
|
<td>${++i}</td>
|
|
<td>${item.name}</td>
|
|
<td class="text-center">${item.quantity}</td>
|
|
<td class="text-end">${formatCurrency(item.price)}</td>
|
|
<td class="text-end">${formatCurrency(total)}</td>
|
|
`;
|
|
}
|
|
const tax = subtotal * 0;
|
|
document.getElementById('invoice-subtotal').textContent = formatCurrency(subtotal);
|
|
document.getElementById('invoice-tax').textContent = formatCurrency(tax);
|
|
document.getElementById('invoice-total').textContent = formatCurrency(subtotal + tax);
|
|
|
|
// Print logic
|
|
const invoiceContent = document.getElementById('invoice-container').innerHTML;
|
|
const printWindow = window.open('', '_blank', 'height=600,width=800');
|
|
printWindow.document.write('<html><head><title>Print Invoice</title>');
|
|
// Include bootstrap for styling
|
|
printWindow.document.write('<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">');
|
|
printWindow.document.write('<style>body { -webkit-print-color-adjust: exact; } @media print { .d-none { display: block !important; } }</style>');
|
|
printWindow.document.write('</head><body>');
|
|
printWindow.document.write(invoiceContent);
|
|
printWindow.document.write('</body></html>');
|
|
printWindow.document.close();
|
|
setTimeout(() => { // Wait for content to load
|
|
printWindow.print();
|
|
}, 500);
|
|
};
|
|
|
|
// --- Event Listeners ---
|
|
barcodeInput.addEventListener('keyup', async (e) => {
|
|
if (e.key === 'Enter') {
|
|
const barcode = barcodeInput.value.trim();
|
|
if (barcode) {
|
|
barcodeInput.disabled = true;
|
|
const found = await findProductByBarcode(barcode);
|
|
if (!found) {
|
|
alert(`Product with barcode "${barcode}" not found.`);
|
|
}
|
|
barcodeInput.value = '';
|
|
barcodeInput.disabled = false;
|
|
barcodeInput.focus();
|
|
}
|
|
}
|
|
});
|
|
|
|
productSearchInput.addEventListener('keyup', debounce((e) => searchProducts(e.target.value.trim()), 300));
|
|
|
|
productGrid.addEventListener('click', (e) => {
|
|
const card = e.target.closest('.product-card');
|
|
if (card) {
|
|
addToCart({
|
|
id: card.dataset.productId,
|
|
name: card.dataset.productName,
|
|
price: card.dataset.productPrice,
|
|
barcode: card.dataset.productBarcode
|
|
});
|
|
}
|
|
});
|
|
|
|
cartItemsContainer.addEventListener('click', (e) => {
|
|
const quantityChangeBtn = e.target.closest('.cart-quantity-change');
|
|
const removeItemBtn = e.target.closest('.cart-remove-item');
|
|
if (quantityChangeBtn) {
|
|
updateCartQuantity(quantityChangeBtn.dataset.productId, parseInt(quantityChangeBtn.dataset.change, 10));
|
|
}
|
|
if (removeItemBtn) {
|
|
removeFromCart(removeItemBtn.dataset.productId);
|
|
}
|
|
});
|
|
|
|
cancelSaleBtn.addEventListener('click', () => {
|
|
if (confirm('Are you sure you want to cancel this sale and clear the cart?')) {
|
|
clearCart();
|
|
}
|
|
});
|
|
|
|
completeSaleBtn.addEventListener('click', completeSale);
|
|
printLastInvoiceBtn.addEventListener('click', printInvoice);
|
|
|
|
// --- Initial Load ---
|
|
renderCart();
|
|
barcodeInput.focus();
|
|
}
|
|
|
|
// --- Logic for Admin Sales Page ---
|
|
if (page === 'admin_sales') {
|
|
const saleDetailsModal = new bootstrap.Modal(document.getElementById('saleDetailsModal'));
|
|
const saleDetailsContent = document.getElementById('saleDetailsContent');
|
|
|
|
document.body.addEventListener('click', async (e) => {
|
|
const detailsButton = e.target.closest('a.btn-outline-info[href*="action=details"]');
|
|
if (detailsButton) {
|
|
e.preventDefault();
|
|
const url = new URL(detailsButton.href);
|
|
const saleId = url.searchParams.get('id');
|
|
|
|
saleDetailsContent.innerHTML = '<p>Loading...</p>';
|
|
saleDetailsModal.show();
|
|
|
|
try {
|
|
const response = await fetch(`api/get_sale_details.php?id=${saleId}`);
|
|
if (!response.ok) throw new Error('Failed to fetch details.');
|
|
const sale = await response.json();
|
|
|
|
let itemsHtml = '<ul class="list-group list-group-flush">';
|
|
sale.items.forEach(item => {
|
|
itemsHtml += `<li class="list-group-item d-flex justify-content-between align-items-center">
|
|
${item.product_name} (x${item.quantity})
|
|
<span>PKR ${parseFloat(item.price_at_sale * item.quantity).toFixed(2)}</span>
|
|
</li>`;
|
|
});
|
|
itemsHtml += '</ul>';
|
|
|
|
saleDetailsContent.innerHTML = `
|
|
<h5>Receipt: ${sale.receipt_number}</h5>
|
|
<p><strong>Cashier:</strong> ${sale.cashier_name || 'N/A'}</p>
|
|
<p><strong>Date:</strong> ${new Date(sale.created_at).toLocaleString()}</p>
|
|
<hr>
|
|
<h6>Items Sold</h6>
|
|
${itemsHtml}
|
|
<hr>
|
|
<div class="text-end">
|
|
<p class="fs-5"><strong>Total: PKR ${parseFloat(sale.total_amount).toFixed(2)}</strong></p>
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
saleDetailsContent.innerHTML = `<p class="text-danger">Error: ${error.message}</p>`;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
'' |