version 2
This commit is contained in:
parent
b6296eed55
commit
4d1d667390
@ -1,18 +1,9 @@
|
||||
'''
|
||||
// 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 ---
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
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');
|
||||
@ -20,345 +11,304 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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');
|
||||
|
||||
// --- State Management ---
|
||||
let cart = JSON.parse(localStorage.getItem('cart')) || {};
|
||||
let cart = [];
|
||||
const TAX_RATE = 0.00;
|
||||
|
||||
// --- Utility Functions ---
|
||||
const debounce = (func, delay) => {
|
||||
let timeout;
|
||||
return function(...args) {
|
||||
const context = this;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(context, args), delay);
|
||||
};
|
||||
// =========================================================================
|
||||
// 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 formatCurrency = (amount) => `PKR ${parseFloat(amount).toFixed(2)}`;
|
||||
|
||||
// --- API Communication ---
|
||||
const searchProducts = async (query) => {
|
||||
const searchProducts = (query, searchBy = 'name') => {
|
||||
if (query.length < 2) {
|
||||
productGrid.innerHTML = '';
|
||||
productGridPlaceholder.style.display = 'block';
|
||||
if (productGridPlaceholder) 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;
|
||||
}
|
||||
};
|
||||
const url = searchBy === 'barcode'
|
||||
? `../api/search_products.php?barcode=${encodeURIComponent(query)}`
|
||||
: `../api/search_products.php?name=${encodeURIComponent(query)}`;
|
||||
|
||||
// --- Rendering Functions ---
|
||||
const renderProductGrid = (products) => {
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(products => {
|
||||
if (productGridPlaceholder) productGridPlaceholder.style.display = 'none';
|
||||
productGrid.innerHTML = '';
|
||||
if (products.length === 0) {
|
||||
productGridPlaceholder.innerHTML = '<p>No products found.</p>';
|
||||
productGridPlaceholder.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
productGridPlaceholder.style.display = 'none';
|
||||
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 = 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}">
|
||||
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 text-muted small">${formatCurrency(product.price)}</p>
|
||||
<p class="card-text fw-bold">PKR ${parseFloat(product.price).toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
productGrid.appendChild(productCard);
|
||||
</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 renderCart = () => {
|
||||
cartItemsContainer.innerHTML = '';
|
||||
let subtotal = 0;
|
||||
let itemCount = 0;
|
||||
|
||||
if (Object.keys(cart).length === 0) {
|
||||
cartItemsContainer.appendChild(cartPlaceholder);
|
||||
const addToCart = (product) => {
|
||||
const existingItem = cart.find(item => item.id === product.id);
|
||||
if (existingItem) {
|
||||
existingItem.quantity++;
|
||||
} else {
|
||||
const table = document.createElement('table');
|
||||
table.className = 'table table-sm';
|
||||
table.innerHTML = `
|
||||
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></th>
|
||||
<th class="text-end">Actions</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>
|
||||
<tbody>
|
||||
${cart.map(item => `
|
||||
<tr>
|
||||
<td class="w-50">${item.name}</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>
|
||||
<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 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 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>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
cartItemsContainer.appendChild(table);
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>`;
|
||||
cartItemsContainer.innerHTML = table;
|
||||
}
|
||||
updateCartTotal();
|
||||
};
|
||||
|
||||
const tax = subtotal * 0;
|
||||
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 = formatCurrency(subtotal);
|
||||
cartTax.textContent = formatCurrency(tax);
|
||||
cartTotal.textContent = formatCurrency(total);
|
||||
cartItemCount.textContent = itemCount;
|
||||
completeSaleBtn.disabled = itemCount === 0;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
// --- Cart Logic ---
|
||||
const saveCart = () => localStorage.setItem('cart', JSON.stringify(cart));
|
||||
const addToCart = (product) => {
|
||||
const id = product.id;
|
||||
if (cart[id]) {
|
||||
cart[id].quantity++;
|
||||
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 {
|
||||
cart[id] = { id: id, name: product.name, price: parseFloat(product.price), quantity: 1, barcode: product.barcode };
|
||||
showToast(`Sale failed: ${data.message}`, 'danger');
|
||||
}
|
||||
saveCart();
|
||||
renderCart();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error completing sale:', error);
|
||||
showToast('An error occurred while completing the sale.', 'danger');
|
||||
});
|
||||
};
|
||||
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;
|
||||
const cancelSale = () => {
|
||||
if (confirm('Are you sure you want to cancel this sale?')) {
|
||||
cart = [];
|
||||
updateCartView();
|
||||
showToast('Sale cancelled.', 'info');
|
||||
}
|
||||
};
|
||||
|
||||
completeSaleBtn.disabled = true;
|
||||
completeSaleBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Processing...';
|
||||
const saveInvoiceForOffline = (saleDetails) => {
|
||||
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';
|
||||
localStorage.setItem('lastInvoice', JSON.stringify(saleDetails));
|
||||
} catch (e) {
|
||||
console.error('Could not save invoice to localStorage:', e);
|
||||
}
|
||||
};
|
||||
|
||||
const printInvoice = () => {
|
||||
const lastSale = JSON.parse(localStorage.getItem('lastSale'));
|
||||
if (!lastSale) {
|
||||
alert('No last sale found to print.');
|
||||
const printInvoice = (invoiceData) => {
|
||||
if (!invoiceData) {
|
||||
showToast('No invoice data to print.', 'warning');
|
||||
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();
|
||||
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 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>
|
||||
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">${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);
|
||||
<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;
|
||||
|
||||
// Print logic
|
||||
const invoiceContent = document.getElementById('invoice-container').innerHTML;
|
||||
const printWindow = window.open('', '_blank', 'height=600,width=800');
|
||||
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>');
|
||||
// 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>');
|
||||
// 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(invoiceContent);
|
||||
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);
|
||||
};
|
||||
|
||||
// --- Event Listeners ---
|
||||
barcodeInput.addEventListener('keyup', async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const barcode = barcodeInput.value.trim();
|
||||
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) {
|
||||
barcodeInput.disabled = true;
|
||||
const found = await findProductByBarcode(barcode);
|
||||
if (!found) {
|
||||
alert(`Product with barcode "${barcode}" not found.`);
|
||||
}
|
||||
barcodeInput.value = '';
|
||||
barcodeInput.disabled = false;
|
||||
barcodeInput.focus();
|
||||
}
|
||||
searchProducts(barcode, 'barcode');
|
||||
}
|
||||
});
|
||||
barcodeInput.focus(); // Keep focus on the barcode input
|
||||
}
|
||||
|
||||
productSearchInput.addEventListener('keyup', debounce((e) => searchProducts(e.target.value.trim()), 300));
|
||||
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) {
|
||||
addToCart({
|
||||
id: card.dataset.productId,
|
||||
name: card.dataset.productName,
|
||||
price: card.dataset.productPrice,
|
||||
barcode: card.dataset.productBarcode
|
||||
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) => {
|
||||
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>`;
|
||||
}
|
||||
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();
|
||||
});
|
||||
''
|
||||
@ -22,7 +22,7 @@ $page = $_GET['page'] ?? ($role === 'admin' ? 'products' : 'checkout'); // Defau
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo filemtime('assets/css/custom.css'); ?>">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body data-page="<?php echo htmlspecialchars($role . '_' . $page); ?>">
|
||||
@ -87,6 +87,6 @@ $page = $_GET['page'] ?? ($role === 'admin' ? 'products' : 'checkout'); // Defau
|
||||
</main>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/js/main.js"></script>
|
||||
<script src="assets/js/main.js?v=<?php echo filemtime('assets/js/main.js'); ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -3,4 +3,5 @@ INSERT INTO `products` (`name`, `description`, `price`, `barcode`) VALUES
|
||||
('Stainless Steel Water Bottle', 'Keeps your drinks cold for 24 hours or hot for 12.', 18.50, '2345678901234'),
|
||||
('Organic Blend Coffee Beans', 'A rich, full-bodied coffee blend, ethically sourced.', 15.75, '3456789012345'),
|
||||
('Wireless Noise-Cancelling Headphones', 'Immerse yourself in sound with these high-fidelity headphones.', 120.00, '4567890123456'),
|
||||
('Handcrafted Wooden Pen', 'A unique, handcrafted pen made from reclaimed oak.', 35.00, '5678901234567');
|
||||
('Handcrafted Wooden Pen', 'A unique, handcrafted pen made from reclaimed oak.', 35.00, '5678901234567'),
|
||||
('Gemini Test Product', 'A special product for testing purposes.', 1.00, '9999999999999');
|
||||
2
db/migrations/003_add_gemini_product.sql
Normal file
2
db/migrations/003_add_gemini_product.sql
Normal file
@ -0,0 +1,2 @@
|
||||
INSERT INTO `products` (`name`, `description`, `price`, `barcode`) VALUES
|
||||
('Gemini Test Product', 'A special product for testing purposes.', 1.00, '9999999999999');
|
||||
@ -1,7 +1,10 @@
|
||||
'''
|
||||
const CACHE_NAME = 'opulent-pos-cache-v1';
|
||||
const CACHE_NAME = 'opulent-pos-cache-v2';
|
||||
const urlsToCache = [
|
||||
'/',
|
||||
'/index.php',
|
||||
'/dashboard.php',
|
||||
'/dashboard.php?view=cashier_checkout',
|
||||
'/manifest.json',
|
||||
'/assets/css/custom.css',
|
||||
'/assets/js/main.js',
|
||||
'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css',
|
||||
@ -9,50 +12,16 @@ const urlsToCache = [
|
||||
];
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
self.skipWaiting(); // Activate worker immediately
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then(cache => {
|
||||
console.log('Opened cache');
|
||||
// Add all the assets to the cache
|
||||
console.log('Opened cache and caching assets');
|
||||
return cache.addAll(urlsToCache);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then(response => {
|
||||
// Cache hit - return response
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Clone the request because it's a stream and can only be consumed once.
|
||||
const fetchRequest = event.request.clone();
|
||||
|
||||
return fetch(fetchRequest).then(
|
||||
response => {
|
||||
// Check if we received a valid response
|
||||
if (!response || response.status !== 200 || response.type !== 'basic') {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Clone the response because it also is a stream
|
||||
const responseToCache = response.clone();
|
||||
|
||||
caches.open(CACHE_NAME)
|
||||
.then(cache => {
|
||||
cache.put(event.request, responseToCache);
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
const cacheWhitelist = [CACHE_NAME];
|
||||
event.waitUntil(
|
||||
@ -60,11 +29,52 @@ self.addEventListener('activate', event => {
|
||||
return Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
if (cacheWhitelist.indexOf(cacheName) === -1) {
|
||||
console.log('Deleting old cache:', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => self.clients.claim()) // Take control of all clients
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
// We only want to cache GET requests
|
||||
if (event.request.method !== 'GET') {
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
caches.open(CACHE_NAME).then(async (cache) => {
|
||||
// Try to get the response from the cache
|
||||
const cachedResponse = await cache.match(event.request);
|
||||
|
||||
// Return the cached response if found
|
||||
if (cachedResponse) {
|
||||
// Re-fetch in the background to update the cache for next time
|
||||
fetch(event.request).then(response => {
|
||||
if (response.ok) {
|
||||
cache.put(event.request, response.clone());
|
||||
}
|
||||
});
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// If not in cache, fetch from the network
|
||||
try {
|
||||
const networkResponse = await fetch(event.request);
|
||||
// If the fetch is successful, clone it and store it in the cache
|
||||
if (networkResponse.ok) {
|
||||
cache.put(event.request, networkResponse.clone());
|
||||
}
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
// If the fetch fails (e.g., offline), return a fallback or an error
|
||||
console.log('Fetch failed; returning offline page instead.', error);
|
||||
// You could return a generic offline page here if you had one
|
||||
// For now, just let the browser handle the error
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
'''
|
||||
Loading…
x
Reference in New Issue
Block a user