315 lines
13 KiB
JavaScript
315 lines
13 KiB
JavaScript
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const barcodeInput = document.getElementById('barcode-scanner-input');
|
|
const productSearchInput = document.getElementById('product-search');
|
|
const productGrid = document.getElementById('product-grid');
|
|
const cartItemsContainer = document.getElementById('cart-items');
|
|
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');
|
|
const cartPlaceholder = document.getElementById('cart-placeholder');
|
|
const productGridPlaceholder = document.getElementById('product-grid-placeholder');
|
|
|
|
let cart = [];
|
|
const TAX_RATE = 0.00;
|
|
|
|
// =========================================================================
|
|
// CORE FUNCTIONS
|
|
// =========================================================================
|
|
|
|
const showToast = (message, type = 'success') => {
|
|
// A simple toast notification function (can be replaced with a library)
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast show align-items-center text-white bg-${type} border-0`;
|
|
toast.innerHTML = `<div class="d-flex"><div class="toast-body">${message}</div><button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
|
|
document.body.appendChild(toast);
|
|
setTimeout(() => toast.remove(), 3000);
|
|
};
|
|
|
|
const searchProducts = (query, searchBy = 'name') => {
|
|
if (query.length < 2) {
|
|
productGrid.innerHTML = '';
|
|
if (productGridPlaceholder) productGridPlaceholder.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
const url = searchBy === 'barcode'
|
|
? `../api/search_products.php?barcode=${encodeURIComponent(query)}`
|
|
: `../api/search_products.php?name=${encodeURIComponent(query)}`;
|
|
|
|
fetch(url)
|
|
.then(response => response.json())
|
|
.then(products => {
|
|
if (productGridPlaceholder) productGridPlaceholder.style.display = 'none';
|
|
productGrid.innerHTML = '';
|
|
if (products.length > 0) {
|
|
if (searchBy === 'barcode' && products.length === 1) {
|
|
addToCart(products[0]);
|
|
barcodeInput.value = ''; // Clear input after successful scan
|
|
barcodeInput.focus();
|
|
} else {
|
|
products.forEach(product => {
|
|
const productCard = `
|
|
<div class="col">
|
|
<div class="card h-100 product-card" data-product-id="${product.id}">
|
|
<div class="card-body text-center">
|
|
<h6 class="card-title fs-sm">${product.name}</h6>
|
|
<p class="card-text fw-bold">PKR ${parseFloat(product.price).toFixed(2)}</p>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
productGrid.innerHTML += productCard;
|
|
});
|
|
}
|
|
} else {
|
|
if (productGridPlaceholder) productGridPlaceholder.style.display = 'block';
|
|
if (searchBy === 'barcode') {
|
|
showToast('Product not found for this barcode.', 'danger');
|
|
barcodeInput.value = '';
|
|
barcodeInput.focus();
|
|
}
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error searching products:', error);
|
|
showToast('Failed to search for products.', 'danger');
|
|
});
|
|
};
|
|
|
|
const addToCart = (product) => {
|
|
const existingItem = cart.find(item => item.id === product.id);
|
|
if (existingItem) {
|
|
existingItem.quantity++;
|
|
} else {
|
|
cart.push({ ...product, quantity: 1 });
|
|
}
|
|
updateCartView();
|
|
showToast(`${product.name} added to cart.`, 'success');
|
|
};
|
|
|
|
const updateCartView = () => {
|
|
if (cart.length === 0) {
|
|
if (cartPlaceholder) cartPlaceholder.style.display = 'block';
|
|
cartItemsContainer.innerHTML = cartPlaceholder ? cartPlaceholder.outerHTML : '';
|
|
} else {
|
|
if (cartPlaceholder) cartPlaceholder.style.display = 'none';
|
|
const table = `
|
|
<table class="table table-sm table-striped">
|
|
<thead>
|
|
<tr>
|
|
<th>Item</th>
|
|
<th class="text-center">Qty</th>
|
|
<th class="text-end">Price</th>
|
|
<th class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${cart.map(item => `
|
|
<tr>
|
|
<td class="w-50">${item.name}</td>
|
|
<td class="text-center">
|
|
<input type="number" class="form-control form-control-sm quantity-input" data-product-id="${item.id}" value="${item.quantity}" min="1">
|
|
</td>
|
|
<td class="text-end">PKR ${parseFloat(item.price).toFixed(2)}</td>
|
|
<td class="text-end">
|
|
<button class="btn btn-outline-danger btn-sm remove-item-btn" data-product-id="${item.id}">
|
|
×
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>`;
|
|
cartItemsContainer.innerHTML = table;
|
|
}
|
|
updateCartTotal();
|
|
};
|
|
|
|
const updateCartTotal = () => {
|
|
const subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
|
const tax = subtotal * TAX_RATE;
|
|
const total = subtotal + tax;
|
|
|
|
cartSubtotal.textContent = `PKR ${subtotal.toFixed(2)}`;
|
|
cartTax.textContent = `PKR ${tax.toFixed(2)}`;
|
|
cartTotal.textContent = `PKR ${total.toFixed(2)}`;
|
|
cartItemCount.textContent = cart.reduce((sum, item) => sum + item.quantity, 0);
|
|
|
|
completeSaleBtn.disabled = cart.length === 0;
|
|
};
|
|
|
|
const completeSale = () => {
|
|
if (cart.length === 0) return;
|
|
|
|
fetch('../api/complete_sale.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ cart: cart })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showToast('Sale completed successfully!', 'success');
|
|
saveInvoiceForOffline(data.sale_details);
|
|
printInvoice(data.sale_details);
|
|
cart = [];
|
|
updateCartView();
|
|
} else {
|
|
showToast(`Sale failed: ${data.message}`, 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error completing sale:', error);
|
|
showToast('An error occurred while completing the sale.', 'danger');
|
|
});
|
|
};
|
|
|
|
const cancelSale = () => {
|
|
if (confirm('Are you sure you want to cancel this sale?')) {
|
|
cart = [];
|
|
updateCartView();
|
|
showToast('Sale cancelled.', 'info');
|
|
}
|
|
};
|
|
|
|
const saveInvoiceForOffline = (saleDetails) => {
|
|
try {
|
|
localStorage.setItem('lastInvoice', JSON.stringify(saleDetails));
|
|
} catch (e) {
|
|
console.error('Could not save invoice to localStorage:', e);
|
|
}
|
|
};
|
|
|
|
const printInvoice = (invoiceData) => {
|
|
if (!invoiceData) {
|
|
showToast('No invoice data to print.', 'warning');
|
|
return;
|
|
}
|
|
|
|
document.getElementById('invoice-receipt-number').textContent = invoiceData.sale_id;
|
|
document.getElementById('invoice-cashier-name').textContent = invoiceData.cashier_name || 'N/A';
|
|
document.getElementById('invoice-date').textContent = new Date(invoiceData.sale_date).toLocaleString();
|
|
|
|
const itemsHtml = invoiceData.items.map((item, index) => `
|
|
<tr>
|
|
<td>${index + 1}</td>
|
|
<td>${item.name}</td>
|
|
<td class="text-center">${item.quantity}</td>
|
|
<td class="text-end">PKR ${parseFloat(item.price).toFixed(2)}</td>
|
|
<td class="text-end">PKR ${(item.quantity * item.price).toFixed(2)}</td>
|
|
</tr>
|
|
`).join('');
|
|
document.getElementById('invoice-items-table').innerHTML = itemsHtml;
|
|
|
|
const subtotal = invoiceData.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
|
const tax = subtotal * TAX_RATE;
|
|
const total = subtotal + tax;
|
|
|
|
document.getElementById('invoice-subtotal').textContent = `PKR ${subtotal.toFixed(2)}`;
|
|
document.getElementById('invoice-tax').textContent = `PKR ${tax.toFixed(2)}`;
|
|
document-getElementById('invoice-total').textContent = `PKR ${total.toFixed(2)}`;
|
|
|
|
const invoiceHtml = document.getElementById('invoice-container').innerHTML;
|
|
const printWindow = window.open('', '_blank');
|
|
printWindow.document.write('<html><head><title>Print Invoice</title>');
|
|
// Optional: Add bootstrap for styling the print view
|
|
printWindow.document.write('<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">');
|
|
printWindow.document.write('</head><body>');
|
|
printWindow.document.write(invoiceHtml.replace('d-none', '')); // Show the invoice
|
|
printWindow.document.write('</body></html>');
|
|
printWindow.document.close();
|
|
setTimeout(() => { // Wait for content to load
|
|
printWindow.print();
|
|
printWindow.close();
|
|
}, 500);
|
|
};
|
|
|
|
const printLastInvoice = () => {
|
|
try {
|
|
const lastInvoice = localStorage.getItem('lastInvoice');
|
|
if (lastInvoice) {
|
|
printInvoice(JSON.parse(lastInvoice));
|
|
} else {
|
|
showToast('No previously saved invoice found.', 'info');
|
|
}
|
|
} catch (e) {
|
|
console.error('Could not retrieve or print last invoice:', e);
|
|
showToast('Failed to print last invoice.', 'danger');
|
|
}
|
|
};
|
|
|
|
|
|
// =========================================================================
|
|
// EVENT LISTENERS
|
|
// =========================================================================
|
|
|
|
if (barcodeInput) {
|
|
barcodeInput.addEventListener('change', (e) => { // 'change' is often better for scanners
|
|
const barcode = e.target.value.trim();
|
|
if (barcode) {
|
|
searchProducts(barcode, 'barcode');
|
|
}
|
|
});
|
|
barcodeInput.focus(); // Keep focus on the barcode input
|
|
}
|
|
|
|
if (productSearchInput) {
|
|
productSearchInput.addEventListener('keyup', (e) => {
|
|
searchProducts(e.target.value.trim(), 'name');
|
|
});
|
|
}
|
|
|
|
if (productGrid) {
|
|
productGrid.addEventListener('click', (e) => {
|
|
const card = e.target.closest('.product-card');
|
|
if (card) {
|
|
const productId = card.dataset.productId;
|
|
// We need to fetch the full product details to add to cart
|
|
fetch(`../api/search_products.php?id=${productId}`)
|
|
.then(res => res.json())
|
|
.then(product => {
|
|
if(product) addToCart(product);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
if (cartItemsContainer) {
|
|
cartItemsContainer.addEventListener('change', (e) => {
|
|
if (e.target.classList.contains('quantity-input')) {
|
|
const productId = parseInt(e.target.dataset.productId);
|
|
const newQuantity = parseInt(e.target.value);
|
|
const itemInCart = cart.find(item => item.id === productId);
|
|
if (itemInCart) {
|
|
if (newQuantity > 0) {
|
|
itemInCart.quantity = newQuantity;
|
|
} else {
|
|
// Remove if quantity is 0 or less
|
|
cart = cart.filter(item => item.id !== productId);
|
|
}
|
|
updateCartView();
|
|
}
|
|
}
|
|
});
|
|
|
|
cartItemsContainer.addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('remove-item-btn')) {
|
|
const productId = parseInt(e.target.dataset.productId);
|
|
cart = cart.filter(item => item.id !== productId);
|
|
updateCartView();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (completeSaleBtn) completeSaleBtn.addEventListener('click', completeSale);
|
|
if (cancelSaleBtn) cancelSaleBtn.addEventListener('click', cancelSale);
|
|
if (printLastInvoiceBtn) printLastInvoiceBtn.addEventListener('click', printLastInvoice);
|
|
|
|
// Initial setup
|
|
updateCartView();
|
|
});
|