941 lines
38 KiB
JavaScript
941 lines
38 KiB
JavaScript
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 = '<div class="text-center py-5"><div class="spinner-border text-primary" role="status"></div></div>';
|
|
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 = `
|
|
<div>
|
|
<div class="fw-bold">Order #${order.id} <span class="badge bg-secondary ms-1">${order.order_type}</span></div>
|
|
<small class="text-muted">
|
|
${order.customer_name || 'Guest'}
|
|
${order.table_number ? ' • Table ' + order.table_number : ''}
|
|
• ${order.time_formatted}
|
|
</small>
|
|
</div>
|
|
<div class="text-end">
|
|
<div class="fw-bold text-primary">${formatCurrency(order.total_amount)}</div>
|
|
<small class="text-muted">${order.item_count} items</small>
|
|
</div>
|
|
`;
|
|
item.onclick = () => loadRecalledOrder(order.id);
|
|
recallList.appendChild(item);
|
|
});
|
|
} else {
|
|
recallList.innerHTML = '<div class="p-4 text-center text-muted">No unpaid bills found.</div>';
|
|
}
|
|
})
|
|
.catch(err => {
|
|
recallList.innerHTML = '<div class="alert alert-danger">Error fetching orders.</div>';
|
|
});
|
|
}
|
|
|
|
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 = `<div class="fw-bold">${cust.name}</div><div class="small text-muted">${cust.phone || ''}</div>`;
|
|
a.onclick = (ev) => {
|
|
ev.preventDefault();
|
|
selectCustomer(cust);
|
|
};
|
|
customerResults.appendChild(a);
|
|
});
|
|
customerResults.style.display = 'block';
|
|
} else {
|
|
customerResults.innerHTML = '<div class="list-group-item text-muted">No results found</div>';
|
|
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 && typeof LOYALTY_SETTINGS !== 'undefined' && LOYALTY_SETTINGS.is_enabled) {
|
|
loyaltySection.classList.remove('d-none');
|
|
loyaltyPointsDisplay.textContent = cust.points + ' pts';
|
|
|
|
if (cust.eligible_for_free_meal) {
|
|
redeemLoyaltyBtn.disabled = false;
|
|
loyaltyMessage.innerHTML = '<span class="text-success fw-bold">Eligible for Free Meal!</span>';
|
|
} 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 (typeof LOYALTY_SETTINGS === 'undefined' || !LOYALTY_SETTINGS.is_enabled) return;
|
|
|
|
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 = '<span class="text-success fw-bold"><i class="bi bi-check-circle"></i> Redeemed! Place order to finalize.</span>';
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- 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 = '<div class="text-center py-5"><div class="spinner-border text-primary" role="status"></div></div>';
|
|
|
|
// 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 = `<div class="alert alert-danger">${data.error}</div>`;
|
|
}
|
|
})
|
|
.catch(() => {
|
|
container.innerHTML = `<div class="alert alert-danger">Error loading tables.</div>`;
|
|
});
|
|
}
|
|
|
|
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 = `<div class="col-12 text-center text-muted py-4">
|
|
<i class="bi bi-slash-circle fs-1 d-block mb-2"></i>
|
|
No tables found for <strong>${outletName}</strong>.
|
|
</div>`;
|
|
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 = `
|
|
<div class="card h-100 shadow-sm ${isOccupied ? 'bg-secondary text-white opacity-50' : 'border-primary text-primary'}"
|
|
style="cursor: ${isOccupied ? 'not-allowed' : 'pointer'}"
|
|
${!isOccupied ? `onclick="selectTable(${table.id}, '${table.name}')"` : ''}>
|
|
<div class="card-body text-center p-3">
|
|
<h5 class="fw-bold mb-1">${table.name}</h5>
|
|
<small>${table.capacity} Pax</small>
|
|
<div class="mt-2 badge ${isOccupied ? 'bg-dark' : 'bg-success'}">${isOccupied ? 'Occupied' : 'Available'}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
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 = `
|
|
<span>${v.name}</span>
|
|
<span class="fw-bold">${formatCurrency(finalPrice)}</span>
|
|
`;
|
|
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 = `
|
|
<div class="text-center text-muted mt-5">
|
|
<i class="bi bi-basket3 fs-1 text-light"></i>
|
|
<p class="mt-2">Cart is empty</p>
|
|
</div>`;
|
|
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 ? `<span class="badge bg-light text-dark border ms-1">${item.variant_name}</span>` : '';
|
|
|
|
row.innerHTML = `
|
|
<div class="flex-grow-1 me-2">
|
|
<div class="fw-bold text-truncate" style="max-width: 140px;">${item.name}</div>
|
|
<div class="small text-muted">${formatCurrency(item.price)} ${variantLabel}</div>
|
|
</div>
|
|
<div class="d-flex align-items-center bg-light rounded px-1">
|
|
<button class="btn btn-sm text-secondary p-0" style="width: 24px;" onclick="changeQuantity(${index}, -1)"><i class="bi bi-dash"></i></button>
|
|
<span class="mx-1 fw-bold small" style="min-width: 20px; text-align: center;">${item.quantity}</span>
|
|
<button class="btn btn-sm text-secondary p-0" style="width: 24px;" onclick="changeQuantity(${index}, 1)"><i class="bi bi-plus"></i></button>
|
|
</div>
|
|
<div class="text-end ms-3" style="min-width: 60px;">
|
|
<div class="fw-bold">${formatCurrency(itemTotal)}</div>
|
|
<button class="btn btn-sm text-danger p-0 mt-1" style="font-size: 0.8rem;" onclick="removeFromCart(${index})">Remove</button>
|
|
</div>
|
|
`;
|
|
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 = `
|
|
<button class="btn btn-outline-primary w-100 payment-btn d-flex flex-column align-items-center justify-content-center"
|
|
onclick="processOrder(${pt.id}, '${pt.name}')">
|
|
<i class="bi ${getPaymentIcon(pt.name)} fs-3 mb-1"></i>
|
|
<span>${pt.name}</span>
|
|
</button>
|
|
`;
|
|
paymentMethodsContainer.appendChild(col);
|
|
});
|
|
} else {
|
|
paymentMethodsContainer.innerHTML = '<div class="alert alert-warning">No payment methods configured.</div>';
|
|
}
|
|
}
|
|
|
|
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 => `
|
|
<tr>
|
|
<td style="padding: 2px 0;">
|
|
${item.name} <br>
|
|
${item.variant_name ? `<small>(${item.variant_name})</small>` : ''}
|
|
</td>
|
|
<td style="text-align: right; vertical-align: top;">${item.quantity} x ${formatCurrency(item.price)}</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
const customerHtml = data.customer ? `
|
|
<div style="margin-bottom: 10px; border-bottom: 1px dashed #000; padding-bottom: 5px;">
|
|
<strong>Customer:</strong><br>
|
|
${data.customer.name}<br>
|
|
${data.customer.phone || ''}
|
|
</div>
|
|
` : '';
|
|
|
|
const tableHtml = data.tableNumber ? `<div>Table: ${data.tableNumber}</div>` : '';
|
|
const paymentHtml = data.paymentMethod ? `<div>Payment: ${data.paymentMethod}</div>` : '';
|
|
const loyaltyHtml = data.loyaltyRedeemed ? `<div><strong>* Free Meal Redeemed *</strong></div>` : '';
|
|
|
|
const html = `
|
|
<html>
|
|
<head>
|
|
<title>Receipt #${data.orderId}</title>
|
|
<style>
|
|
body { font-family: 'Courier New', monospace; font-size: 12px; margin: 0; padding: 10px; }
|
|
.header { text-align: center; margin-bottom: 10px; }
|
|
.header h2 { margin: 0; font-size: 16px; }
|
|
table { width: 100%; border-collapse: collapse; }
|
|
.totals { margin-top: 10px; border-top: 1px dashed #000; padding-top: 5px; }
|
|
.footer { text-align: center; margin-top: 20px; font-size: 10px; }
|
|
@media print {
|
|
.no-print { display: none; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h2>FLATLOGIC POS</h2>
|
|
<div>123 Main St, City</div>
|
|
<div>Tel: 123-456-7890</div>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 10px;">
|
|
<div>Order #${data.orderId}</div>
|
|
<div>${data.date}</div>
|
|
<div>Type: ${data.orderType.toUpperCase()}</div>
|
|
${tableHtml}
|
|
${paymentHtml}
|
|
${loyaltyHtml}
|
|
</div>
|
|
|
|
${customerHtml}
|
|
|
|
<table>
|
|
${itemsHtml}
|
|
</table>
|
|
|
|
<div class="totals">
|
|
<table style="width: 100%">
|
|
<tr>
|
|
<td>Subtotal</td>
|
|
<td style="text-align: right">${formatCurrency(data.total + data.discount)}</td>
|
|
</tr>
|
|
${data.discount > 0 ? `
|
|
<tr>
|
|
<td>Discount</td>
|
|
<td style="text-align: right">-${formatCurrency(data.discount)}</td>
|
|
</tr>` : ''}
|
|
<tr style="font-weight: bold; font-size: 14px;">
|
|
<td>Total</td>
|
|
<td style="text-align: right">${formatCurrency(data.total)}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
Thank you for your visit!<br>
|
|
Please come again.
|
|
</div>
|
|
|
|
<script>
|
|
window.onload = function() {
|
|
window.print();
|
|
setTimeout(function() { window.close(); }, 500);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
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');
|
|
});
|
|
});
|
|
}
|
|
});
|