document.addEventListener('DOMContentLoaded', () => { // SweetAlert2 Toast Mixin const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, timerProgressBar: true, didOpen: (toast) => { toast.addEventListener('mouseenter', Swal.stopTimer); toast.addEventListener('mouseleave', Swal.resumeTimer); } }); let cart = []; let currentOrderId = null; // Track order ID for updates const cartItemsContainer = document.getElementById('cart-items'); const cartTotalPrice = document.getElementById('cart-total-price'); const cartSubtotal = document.getElementById('cart-subtotal'); const cartDiscountInput = document.getElementById('cart-discount-input'); // Updated Button References const quickOrderBtn = document.getElementById('quick-order-btn'); const placeOrderBtn = document.getElementById('place-order-btn'); const recallBtn = document.getElementById('recall-bill-btn'); // Recall Modal const recallModalEl = document.getElementById('recallOrderModal'); const recallModal = recallModalEl ? new bootstrap.Modal(recallModalEl) : null; const recallList = document.getElementById('recall-orders-list'); // Loyalty State let isLoyaltyRedemption = false; const loyaltySection = document.getElementById('loyalty-section'); const loyaltyPointsDisplay = document.getElementById('loyalty-points-display'); const loyaltyMessage = document.getElementById('loyalty-message'); const redeemLoyaltyBtn = document.getElementById('redeem-loyalty-btn'); // Table Management let currentTableId = null; let currentTableName = null; const tableDisplay = document.getElementById('current-table-display'); const tableModalEl = document.getElementById('tableSelectionModal'); const tableSelectionModal = new bootstrap.Modal(tableModalEl); // Variant Management const variantModalEl = document.getElementById('variantSelectionModal'); const variantSelectionModal = new bootstrap.Modal(variantModalEl); let pendingProduct = null; // Customer Search Elements const customerSearchInput = document.getElementById('customer-search'); const customerResults = document.getElementById('customer-results'); const selectedCustomerId = document.getElementById('selected-customer-id'); const clearCustomerBtn = document.getElementById('clear-customer'); const customerInfo = document.getElementById('customer-info'); const customerNameDisplay = document.getElementById('customer-name-display'); let currentCustomer = null; // Payment Modal const paymentModalEl = document.getElementById('paymentSelectionModal'); const paymentSelectionModal = new bootstrap.Modal(paymentModalEl); const paymentMethodsContainer = document.getElementById('payment-methods-container'); // Product Search & Filter const productSearchInput = document.getElementById('product-search-input'); let currentCategory = 'all'; let currentSearchQuery = ''; // Helper for currency function formatCurrency(amount) { const settings = (typeof COMPANY_SETTINGS !== 'undefined') ? COMPANY_SETTINGS : { currency_symbol: '$', currency_decimals: 2 }; const symbol = settings.currency_symbol || '$'; const decimals = parseInt(settings.currency_decimals || 2); return symbol + parseFloat(amount).toFixed(decimals); } // --- Product Filtering (Category + Search) --- function filterProducts() { const items = document.querySelectorAll('.product-item'); items.forEach(item => { const matchesCategory = (currentCategory == 'all' || item.dataset.categoryId == currentCategory); const name = item.querySelector('.card-title').textContent.toLowerCase(); const matchesSearch = name.includes(currentSearchQuery); if (matchesCategory && matchesSearch) { item.style.display = 'block'; } else { item.style.display = 'none'; } }); } window.filterCategory = function(categoryId, btnElement) { currentCategory = categoryId; // Update Active Button State if (btnElement) { document.querySelectorAll('.category-btn').forEach(btn => btn.classList.remove('active')); btnElement.classList.add('active'); } else if (typeof btnElement === 'undefined' && categoryId !== 'all') { // Try to find the button corresponding to this category ID } filterProducts(); }; if (productSearchInput) { productSearchInput.addEventListener('input', (e) => { currentSearchQuery = e.target.value.trim().toLowerCase(); filterProducts(); }); } // --- Recall Order Logic --- if (recallBtn) { recallBtn.addEventListener('click', () => { fetchRecallOrders(); }); } function fetchRecallOrders() { if (!recallList) return; recallList.innerHTML = '
'; if (recallModal) recallModal.show(); const outletId = new URLSearchParams(window.location.search).get('outlet_id') || 1; fetch(`api/recall_orders.php?action=list&outlet_id=${outletId}`) .then(res => res.json()) .then(data => { recallList.innerHTML = ''; if (data.success && data.orders.length > 0) { data.orders.forEach(order => { const item = document.createElement('button'); item.className = 'list-group-item list-group-item-action d-flex justify-content-between align-items-center'; item.innerHTML = `
Order #${order.id} ${order.order_type}
${order.customer_name || 'Guest'} ${order.table_number ? ' • Table ' + order.table_number : ''} • ${order.time_formatted}
${formatCurrency(order.total_amount)}
${order.item_count} items
`; item.onclick = () => loadRecalledOrder(order.id); recallList.appendChild(item); }); } else { recallList.innerHTML = '
No unpaid bills found.
'; } }) .catch(err => { recallList.innerHTML = '
Error fetching orders.
'; }); } function loadRecalledOrder(orderId) { fetch(`api/recall_orders.php?action=details&id=${orderId}`) .then(res => res.json()) .then(data => { if (data.success) { // Set Order ID currentOrderId = data.order.id; // Set Customer if (data.customer) { selectCustomer(data.customer); } else { if (clearCustomerBtn) clearCustomerBtn.click(); } // Set Table/Order Type const otInput = document.querySelector(`input[name="order_type"][value="${data.order.order_type}"]`); if (otInput) { otInput.checked = true; if (data.order.order_type === 'dine-in' && data.order.table_id) { selectTable(data.order.table_id, data.order.table_number); } else { checkOrderType(); } } // Populate Cart cart = data.items; // Assuming format matches cartDiscountInput.value = data.order.discount || 0; updateCart(); if (recallModal) recallModal.hide(); showToast(`Order #${orderId} loaded!`, 'success'); } else { showToast(data.error || 'Failed to load order', 'danger'); } }) .catch(err => showToast('Error loading order details', 'danger')); } // --- Customer Search --- let searchTimeout; if (customerSearchInput) { customerSearchInput.addEventListener('input', (e) => { const query = e.target.value.trim(); clearTimeout(searchTimeout); if (query.length < 2) { customerResults.style.display = 'none'; return; } searchTimeout = setTimeout(() => { fetch(`api/search_customers.php?q=${encodeURIComponent(query)}`) .then(res => res.json()) .then(data => { customerResults.innerHTML = ''; if (data.length > 0) { data.forEach(cust => { const a = document.createElement('a'); a.href = '#'; a.className = 'list-group-item list-group-item-action'; a.innerHTML = `
${cust.name}
${cust.phone || ''}
`; a.onclick = (ev) => { ev.preventDefault(); selectCustomer(cust); }; customerResults.appendChild(a); }); customerResults.style.display = 'block'; } else { customerResults.innerHTML = '
No results found
'; customerResults.style.display = 'block'; } }); }, 300); }); // Close search on click outside document.addEventListener('click', (e) => { if (!customerSearchInput.contains(e.target) && !customerResults.contains(e.target)) { customerResults.style.display = 'none'; } }); } function selectCustomer(cust) { currentCustomer = cust; selectedCustomerId.value = cust.id; customerNameDisplay.textContent = cust.name; customerSearchInput.value = cust.name; // Show name in input customerSearchInput.disabled = true; // Lock input customerResults.style.display = 'none'; clearCustomerBtn.classList.remove('d-none'); // customerInfo.classList.remove('d-none'); // Loyalty Logic if (loyaltySection) { loyaltySection.classList.remove('d-none'); loyaltyPointsDisplay.textContent = cust.points + ' pts'; if (cust.eligible_for_free_meal) { redeemLoyaltyBtn.disabled = false; loyaltyMessage.innerHTML = 'Eligible for Free Meal!'; } else { redeemLoyaltyBtn.disabled = true; const needed = cust.points_needed || 0; loyaltyMessage.textContent = `${needed} pts away from a free meal.`; } } // Reset redemption state when switching customer (though usually we clear first) isLoyaltyRedemption = false; } if (clearCustomerBtn) { clearCustomerBtn.addEventListener('click', () => { currentCustomer = null; selectedCustomerId.value = ''; customerSearchInput.value = ''; customerSearchInput.disabled = false; clearCustomerBtn.classList.add('d-none'); customerInfo.classList.add('d-none'); // Hide Loyalty if (loyaltySection) loyaltySection.classList.add('d-none'); isLoyaltyRedemption = false; cartDiscountInput.value = 0; updateCart(); customerSearchInput.focus(); }); } // Loyalty Redeem Click if (redeemLoyaltyBtn) { redeemLoyaltyBtn.addEventListener('click', () => { if (cart.length === 0) { showToast("Cart is empty!", "warning"); return; } // --- NEW RESTRICTION --- if (cart.length > 1) { showToast("Can only redeem a free meal with a single item in cart!", "warning"); return; } if (!currentCustomer || !currentCustomer.eligible_for_free_meal) return; Swal.fire({ title: 'Redeem Loyalty?', text: "Redeem 70 points for a free meal? This will apply a full discount to the current order.", icon: 'question', showCancelButton: true, confirmButtonColor: '#198754', cancelButtonColor: '#6c757d', confirmButtonText: 'Yes, redeem it!' }).then((result) => { if (result.isConfirmed) { isLoyaltyRedemption = true; // Calculate total and apply as discount const subtotal = cart.reduce((acc, item) => acc + (item.price * item.quantity), 0); cartDiscountInput.value = subtotal.toFixed(2); updateCart(); showToast("Loyalty Redemption Applied!", "success"); redeemLoyaltyBtn.disabled = true; // Prevent double click loyaltyMessage.innerHTML = ' Redeemed! Place order to finalize.'; } }); }); } // --- Table & Order Type Logic --- const orderTypeInputs = document.querySelectorAll('input[name="order_type"]'); function checkOrderType() { const checked = document.querySelector('input[name="order_type"]:checked'); if (!checked) return; const selected = checked.value; if (selected === 'dine-in') { if (!currentTableId) openTableSelectionModal(); if (tableDisplay) tableDisplay.style.display = 'inline-block'; } else { if (tableDisplay) tableDisplay.style.display = 'none'; } } orderTypeInputs.forEach(input => { input.addEventListener('change', checkOrderType); }); function openTableSelectionModal() { const container = document.getElementById('table-list-container'); container.innerHTML = '
'; // Update Modal Title const modalTitle = document.querySelector('#tableSelectionModal .modal-title'); if (modalTitle && typeof CURRENT_OUTLET !== 'undefined') { modalTitle.textContent = `Select Table - ${CURRENT_OUTLET.name}`; } tableSelectionModal.show(); const outletId = new URLSearchParams(window.location.search).get('outlet_id') || 1; fetch(`api/tables.php?outlet_id=${outletId}`) .then(res => res.json()) .then(data => { if (data.success) { renderTables(data.tables); } else { container.innerHTML = `
${data.error}
`; } }) .catch(() => { container.innerHTML = `
Error loading tables.
`; }); } function renderTables(tables) { const container = document.getElementById('table-list-container'); container.innerHTML = ''; if (tables.length === 0) { const outletName = (typeof CURRENT_OUTLET !== 'undefined') ? CURRENT_OUTLET.name : 'this outlet'; container.innerHTML = `
No tables found for ${outletName}.
`; return; } tables.forEach(table => { const isOccupied = table.is_occupied; const col = document.createElement('div'); col.className = 'col-6 col-md-4 col-lg-3'; col.innerHTML = `
${table.name}
${table.capacity} Pax
${isOccupied ? 'Occupied' : 'Available'}
`; container.appendChild(col); }); } window.selectTable = function(id, name) { currentTableId = id; currentTableName = name; if (tableDisplay) { tableDisplay.innerHTML = `Table: ${name}`; tableDisplay.style.display = 'block'; } tableSelectionModal.hide(); showToast(`Selected Table: ${name}`, 'success'); }; // --- Cart Logic --- document.querySelectorAll('.add-to-cart').forEach(card => { card.addEventListener('click', (e) => { const target = e.currentTarget; const product = { id: target.dataset.id, name: target.dataset.name, price: parseFloat(target.dataset.price), base_price: parseFloat(target.dataset.price), hasVariants: target.dataset.hasVariants === 'true', quantity: 1, variant_id: null, variant_name: null }; if (product.hasVariants) { openVariantModal(product); } else { addToCart(product); } }); }); function openVariantModal(product) { pendingProduct = product; const variants = PRODUCT_VARIANTS[product.id] || []; const list = document.getElementById('variant-list'); const title = document.getElementById('variantModalTitle'); title.textContent = `Select option for ${product.name}`; list.innerHTML = ''; variants.forEach(v => { const btn = document.createElement('button'); btn.className = 'list-group-item list-group-item-action d-flex justify-content-between align-items-center'; const adj = parseFloat(v.price_adjustment); const sign = adj > 0 ? '+' : ''; const finalPrice = product.base_price + adj; btn.innerHTML = ` ${v.name} ${formatCurrency(finalPrice)} `; btn.onclick = () => { pendingProduct.variant_id = v.id; pendingProduct.variant_name = v.name; pendingProduct.price = finalPrice; addToCart(pendingProduct); variantSelectionModal.hide(); }; list.appendChild(btn); }); variantSelectionModal.show(); } function addToCart(product) { const existing = cart.find(item => item.id === product.id && item.variant_id === product.variant_id); if (existing) { existing.quantity++; } else { cart.push({...product}); } updateCart(); } window.changeQuantity = function(index, delta) { if (cart[index]) { cart[index].quantity += delta; if (cart[index].quantity <= 0) { removeFromCart(index); } else { updateCart(); } } }; function updateCart() { if (cart.length === 0) { cartItemsContainer.innerHTML = `

Cart is empty

`; cartSubtotal.innerText = formatCurrency(0); cartTotalPrice.innerText = formatCurrency(0); if (quickOrderBtn) quickOrderBtn.disabled = true; if (placeOrderBtn) placeOrderBtn.disabled = true; // RESET current Order ID if cart becomes empty? // Actually, if we empty the cart, we might want to "cancel" the update? // No, user can add items back. But if they leave it empty, we can't submit. // If they start adding items, it's still the recalled order. // What if they want to START NEW? They should reload or we should provide a Clear button. // For now, let's assume they continue working on it. // If they want new, they can refresh or we can add a "New Order" button later. return; } cartItemsContainer.innerHTML = ''; let subtotal = 0; cart.forEach((item, index) => { const itemTotal = item.price * item.quantity; subtotal += itemTotal; const row = document.createElement('div'); row.className = 'd-flex justify-content-between align-items-center mb-3 border-bottom pb-2'; const variantLabel = item.variant_name ? `${item.variant_name}` : ''; row.innerHTML = `
${item.name}
${formatCurrency(item.price)} ${variantLabel}
${item.quantity}
${formatCurrency(itemTotal)}
`; cartItemsContainer.appendChild(row); }); cartSubtotal.innerText = formatCurrency(subtotal); let discount = parseFloat(cartDiscountInput.value) || 0; if (isLoyaltyRedemption) { discount = subtotal; cartDiscountInput.value = subtotal.toFixed(2); } let total = subtotal - discount; if (total < 0) total = 0; cartTotalPrice.innerText = formatCurrency(total); if (quickOrderBtn) quickOrderBtn.disabled = false; if (placeOrderBtn) placeOrderBtn.disabled = false; } if (cartDiscountInput) { cartDiscountInput.addEventListener('input', () => { updateCart(); }); } window.removeFromCart = function(index) { cart.splice(index, 1); updateCart(); }; // --- Payment Selection Logic --- function renderPaymentMethods() { if (!paymentMethodsContainer) return; paymentMethodsContainer.innerHTML = ''; if (typeof PAYMENT_TYPES !== 'undefined' && PAYMENT_TYPES.length > 0) { PAYMENT_TYPES.forEach(pt => { const col = document.createElement('div'); col.className = 'col-6'; col.innerHTML = ` `; paymentMethodsContainer.appendChild(col); }); } else { paymentMethodsContainer.innerHTML = '
No payment methods configured.
'; } } function getPaymentIcon(name) { const n = name.toLowerCase(); if (n.includes('cash')) return 'bi-cash-coin'; if (n.includes('card') || n.includes('visa') || n.includes('master')) return 'bi-credit-card'; if (n.includes('qr') || n.includes('scan')) return 'bi-qr-code'; return 'bi-wallet2'; } renderPaymentMethods(); // --- Checkout Flow (Quick Order) --- function validateOrder() { if (cart.length === 0) return false; const orderTypeInput = document.querySelector('input[name="order_type"]:checked'); const orderType = orderTypeInput ? orderTypeInput.value : 'takeaway'; if (orderType === 'dine-in' && !currentTableId) { showToast('Please select a table first', 'warning'); openTableSelectionModal(); return false; } return true; } if (quickOrderBtn) { quickOrderBtn.addEventListener('click', () => { if (validateOrder()) { paymentSelectionModal.show(); } }); } // --- Place Order (Pay Later) Flow --- if (placeOrderBtn) { placeOrderBtn.addEventListener('click', () => { if (validateOrder()) { Swal.fire({ title: 'Place Order?', text: "Place order without immediate payment?", icon: 'warning', showCancelButton: true, confirmButtonColor: '#ffc107', cancelButtonColor: '#6c757d', confirmButtonText: 'Yes, place order' }).then((result) => { if (result.isConfirmed) { processOrder(null, 'Pay Later'); } }); } }); } window.processOrder = function(paymentTypeId, paymentTypeName) { const orderTypeInput = document.querySelector('input[name="order_type"]:checked'); const orderType = orderTypeInput ? orderTypeInput.value : 'takeaway'; const subtotal = cart.reduce((acc, item) => acc + (item.price * item.quantity), 0); const discount = parseFloat(cartDiscountInput.value) || 0; const totalAmount = Math.max(0, subtotal - discount); const custId = selectedCustomerId.value; const orderData = { order_id: currentOrderId, // Include ID if updating table_number: (orderType === 'dine-in') ? currentTableId : null, order_type: orderType, customer_id: custId || null, outlet_id: new URLSearchParams(window.location.search).get('outlet_id') || 1, payment_type_id: paymentTypeId, total_amount: totalAmount, discount: discount, redeem_loyalty: isLoyaltyRedemption, items: cart.map(item => ({ product_id: item.id, quantity: item.quantity, unit_price: item.price, variant_id: item.variant_id })) }; // Disable buttons if (paymentMethodsContainer) { const btns = paymentMethodsContainer.querySelectorAll('button'); btns.forEach(b => b.disabled = true); } if (quickOrderBtn) quickOrderBtn.disabled = true; if (placeOrderBtn) placeOrderBtn.disabled = true; fetch('api/order.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(orderData) }) .then(res => res.json()) .then(data => { if (paymentMethodsContainer) { const btns = paymentMethodsContainer.querySelectorAll('button'); btns.forEach(b => b.disabled = false); } if (quickOrderBtn) quickOrderBtn.disabled = false; if (placeOrderBtn) placeOrderBtn.disabled = false; paymentSelectionModal.hide(); if (data.success) { // Print Receipt printReceipt({ orderId: data.order_id, customer: currentCustomer, items: [...cart], total: totalAmount, discount: discount, orderType: orderType, tableNumber: (orderType === 'dine-in') ? currentTableName : null, date: new Date().toLocaleString(), paymentMethod: paymentTypeName, loyaltyRedeemed: isLoyaltyRedemption }); cart = []; cartDiscountInput.value = 0; currentOrderId = null; // Reset isLoyaltyRedemption = false; // Reset updateCart(); if (clearCustomerBtn) clearCustomerBtn.click(); showToast(`Order #${data.order_id} placed!`, 'success'); } else { showToast(`Error: ${data.error}`, 'danger'); } }) .catch(err => { if (paymentMethodsContainer) { const btns = paymentMethodsContainer.querySelectorAll('button'); btns.forEach(b => b.disabled = false); } if (quickOrderBtn) quickOrderBtn.disabled = false; if (placeOrderBtn) placeOrderBtn.disabled = false; paymentSelectionModal.hide(); showToast('Network Error', 'danger'); }); }; function showToast(msg, type = 'primary') { let icon = 'info'; if (type === 'success') icon = 'success'; if (type === 'danger') icon = 'error'; if (type === 'warning') icon = 'warning'; Toast.fire({ icon: icon, title: msg }); } function printReceipt(data) { const width = 300; const height = 600; const left = (screen.width - width) / 2; const top = (screen.height - height) / 2; const win = window.open('', 'Receipt', `width=${width},height=${height},top=${top},left=${left}`); const itemsHtml = data.items.map(item => ` ${item.name}
${item.variant_name ? `(${item.variant_name})` : ''} ${item.quantity} x ${formatCurrency(item.price)} `).join(''); const customerHtml = data.customer ? `
Customer:
${data.customer.name}
${data.customer.phone || ''}
` : ''; const tableHtml = data.tableNumber ? `
Table: ${data.tableNumber}
` : ''; const paymentHtml = data.paymentMethod ? `
Payment: ${data.paymentMethod}
` : ''; const loyaltyHtml = data.loyaltyRedeemed ? `
* Free Meal Redeemed *
` : ''; const html = ` Receipt #${data.orderId}

FLATLOGIC POS

123 Main St, City
Tel: 123-456-7890
Order #${data.orderId}
${data.date}
Type: ${data.orderType.toUpperCase()}
${tableHtml} ${paymentHtml} ${loyaltyHtml}
${customerHtml} ${itemsHtml}
${data.discount > 0 ? ` ` : ''}
Subtotal ${formatCurrency(data.total + data.discount)}
Discount -${formatCurrency(data.discount)}
Total ${formatCurrency(data.total)}
`; win.document.write(html); win.document.close(); } // Initialize logic const urlParams = new URLSearchParams(window.location.search); if (!urlParams.has('order_type')) { const otTakeaway = document.getElementById('ot-takeaway'); if (otTakeaway) { otTakeaway.checked = true; } } checkOrderType(); // --- Add Customer Logic --- const addCustomerBtn = document.getElementById('add-customer-btn'); const addCustomerModalEl = document.getElementById('addCustomerModal'); if (addCustomerBtn && addCustomerModalEl) { const addCustomerModal = new bootstrap.Modal(addCustomerModalEl); const saveCustomerBtn = document.getElementById('save-new-customer'); const newCustomerName = document.getElementById('new-customer-name'); const newCustomerPhone = document.getElementById('new-customer-phone'); const phoneError = document.getElementById('phone-error'); addCustomerBtn.addEventListener('click', () => { newCustomerName.value = ''; newCustomerPhone.value = ''; phoneError.classList.add('d-none'); addCustomerModal.show(); }); saveCustomerBtn.addEventListener('click', () => { const name = newCustomerName.value.trim(); const phone = newCustomerPhone.value.trim(); if (name === '') { showToast('Name is required', 'warning'); return; } // 8 digits validation if (!/^\d{8}$/.test(phone)) { phoneError.classList.remove('d-none'); return; } else { phoneError.classList.add('d-none'); } saveCustomerBtn.disabled = true; saveCustomerBtn.textContent = 'Saving...'; fetch('api/create_customer.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name, phone: phone }) }) .then(res => res.json()) .then(data => { saveCustomerBtn.disabled = false; saveCustomerBtn.textContent = 'Save Customer'; if (data.success) { addCustomerModal.hide(); selectCustomer(data.customer); showToast('Customer created successfully', 'success'); } else { showToast(data.error || 'Error creating customer', 'danger'); } }) .catch(err => { saveCustomerBtn.disabled = false; saveCustomerBtn.textContent = 'Save Customer'; showToast('Network error', 'danger'); }); }); } });