redeem and notifications

This commit is contained in:
Flatlogic Bot 2026-02-23 03:07:51 +00:00
parent f9d2ca374d
commit 6c8b522da6
7 changed files with 156 additions and 44 deletions

View File

@ -1,5 +1,40 @@
</div> <!-- End Main Content --> </div> <!-- End Main Content -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Global SweetAlert2 replacement for confirm()
document.addEventListener('DOMContentLoaded', function() {
const confirmLinks = document.querySelectorAll('a[onclick^="return confirm"]');
confirmLinks.forEach(link => {
const originalOnClick = link.getAttribute('onclick');
// Extract the message from confirm('...')
const match = originalOnClick.match(/confirm\(['"](.+?)['"]\)/);
const message = match ? match[1] : 'Are you sure?';
// Remove the original onclick
link.removeAttribute('onclick');
// Add new click listener
link.addEventListener('click', function(e) {
e.preventDefault(); // Prevent default navigation
Swal.fire({
title: 'Are you sure?',
text: message,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Yes, proceed!'
}).then((result) => {
if (result.isConfirmed) {
window.location.href = this.href;
}
});
});
});
});
</script>
</body> </body>
</html> </html>

View File

@ -35,6 +35,7 @@ function isActive($page) {
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/custom.css?v=<?= time() ?>"> <link rel="stylesheet" href="../assets/css/custom.css?v=<?= time() ?>">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style> <style>
body { font-family: 'Inter', sans-serif; background-color: #f8f9fa; } body { font-family: 'Inter', sans-serif; background-color: #f8f9fa; }
.sidebar { height: 100vh; position: fixed; top: 0; left: 0; width: 250px; background: #fff; border-right: 1px solid #eee; padding-top: 0; z-index: 1000; overflow-y: auto; } .sidebar { height: 100vh; position: fixed; top: 0; left: 0; width: 250px; background: #fff; border-right: 1px solid #eee; padding-top: 0; z-index: 1000; overflow-y: auto; }
@ -193,4 +194,4 @@ function isActive($page) {
</div> </div>
</div> </div>
<div class="main-content pt-5 pt-md-4"> <div class="main-content pt-5 pt-md-4">

View File

@ -102,6 +102,14 @@ try {
$deductStmt = $pdo->prepare("UPDATE customers SET points = points - ? WHERE id = ?"); $deductStmt = $pdo->prepare("UPDATE customers SET points = points - ? WHERE id = ?");
$deductStmt->execute([$points_threshold, $customer_id]); $deductStmt->execute([$points_threshold, $customer_id]);
$points_deducted = $points_threshold; $points_deducted = $points_threshold;
// --- OVERRIDE PAYMENT TYPE ---
$ptStmt = $pdo->prepare("SELECT id FROM payment_types WHERE name = 'Loyalty Redeem' LIMIT 1");
$ptStmt->execute();
$loyaltyPt = $ptStmt->fetchColumn();
if ($loyaltyPt) {
$data['payment_type_id'] = $loyaltyPt;
}
} }
// Award Points // Award Points
@ -239,4 +247,4 @@ You've earned *{points_earned} points* with this order.
} }
error_log("Order Error: " . $e->getMessage()); error_log("Order Error: " . $e->getMessage());
echo json_encode(['success' => false, 'error' => $e->getMessage()]); echo json_encode(['success' => false, 'error' => $e->getMessage()]);
} }

View File

@ -1,4 +1,17 @@
document.addEventListener('DOMContentLoaded', () => { 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 cart = [];
const cartItemsContainer = document.getElementById('cart-items'); const cartItemsContainer = document.getElementById('cart-items');
const cartTotalPrice = document.getElementById('cart-total-price'); const cartTotalPrice = document.getElementById('cart-total-price');
@ -193,20 +206,37 @@ document.addEventListener('DOMContentLoaded', () => {
showToast("Cart is empty!", "warning"); showToast("Cart is empty!", "warning");
return; 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; if (!currentCustomer || !currentCustomer.eligible_for_free_meal) return;
if (confirm("Redeem 70 points for a free meal? This will apply a full discount to the current order.")) { Swal.fire({
isLoyaltyRedemption = true; title: 'Redeem Loyalty?',
text: "Redeem 70 points for a free meal? This will apply a full discount to the current order.",
// Calculate total and apply as discount icon: 'question',
const subtotal = cart.reduce((acc, item) => acc + (item.price * item.quantity), 0); showCancelButton: true,
cartDiscountInput.value = subtotal.toFixed(2); confirmButtonColor: '#198754',
updateCart(); cancelButtonColor: '#6c757d',
confirmButtonText: 'Yes, redeem it!'
showToast("Loyalty Redemption Applied!", "success"); }).then((result) => {
redeemLoyaltyBtn.disabled = true; // Prevent double click if (result.isConfirmed) {
loyaltyMessage.innerHTML = '<span class="text-success fw-bold"><i class="bi bi-check-circle"></i> Redeemed! Place order to finalize.</span>'; 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>';
}
});
}); });
} }
@ -508,9 +538,19 @@ document.addEventListener('DOMContentLoaded', () => {
if (placeOrderBtn) { if (placeOrderBtn) {
placeOrderBtn.addEventListener('click', () => { placeOrderBtn.addEventListener('click', () => {
if (validateOrder()) { if (validateOrder()) {
if (confirm("Place order without immediate payment?")) { Swal.fire({
processOrder(null, 'Pay Later'); 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');
}
});
} }
}); });
} }
@ -603,21 +643,15 @@ document.addEventListener('DOMContentLoaded', () => {
}; };
function showToast(msg, type = 'primary') { function showToast(msg, type = 'primary') {
const toastContainer = document.getElementById('toast-container'); let icon = 'info';
const id = 'toast-' + Date.now(); if (type === 'success') icon = 'success';
const html = ` if (type === 'danger') icon = 'error';
<div id="${id}" class="toast align-items-center text-white bg-${type} border-0" role="alert" aria-live="assertive" aria-atomic="true"> if (type === 'warning') icon = 'warning';
<div class="d-flex">
<div class="toast-body">${msg}</div> Toast.fire({
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button> icon: icon,
</div> title: msg
</div> });
`;
toastContainer.insertAdjacentHTML('beforeend', html);
const el = document.getElementById(id);
const t = new bootstrap.Toast(el, { delay: 3000 });
t.show();
el.addEventListener('hidden.bs.toast', () => el.remove());
} }
function printReceipt(data) { function printReceipt(data) {

View File

@ -0,0 +1,3 @@
INSERT INTO payment_types (name, type, is_active)
SELECT 'Loyalty Redeem', 'cash', 0
WHERE NOT EXISTS (SELECT 1 FROM payment_types WHERE name = 'Loyalty Redeem');

View File

@ -13,6 +13,7 @@ $current_outlet_id = isset($_GET['outlet_id']) ? (int)$_GET['outlet_id'] : 1;
<!-- Re-using existing CSS if any, or adding Bootstrap/Custom --> <!-- Re-using existing CSS if any, or adding Bootstrap/Custom -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="assets/css/custom.css" rel="stylesheet"> <link href="assets/css/custom.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style> <style>
body { background-color: #f4f6f9; } body { background-color: #f4f6f9; }
.order-card { .order-card {
@ -99,7 +100,9 @@ function renderOrders(orders) {
return; return;
} }
grid.innerHTML = orders.map(order => { // Build the HTML using a variable, so we don't clear/flash the grid if it's identical
// But for now, simple innerHTML replacement is fine for this MVP.
const newHtml = orders.map(order => {
let actionBtn = ''; let actionBtn = '';
let statusBadge = ''; let statusBadge = '';
let cardClass = 'order-card '; let cardClass = 'order-card ';
@ -151,11 +154,27 @@ function renderOrders(orders) {
</div> </div>
`; `;
}).join(''); }).join('');
grid.innerHTML = newHtml;
} }
async function updateStatus(orderId, newStatus) { function updateStatus(orderId, newStatus) {
if (!confirm(`Move order #${orderId} to ${newStatus}?`)) return; Swal.fire({
title: 'Update Status?',
text: `Move order #${orderId} to ${newStatus}?`,
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, update it!'
}).then((result) => {
if (result.isConfirmed) {
performUpdate(orderId, newStatus);
}
});
}
async function performUpdate(orderId, newStatus) {
try { try {
const response = await fetch('api/kitchen.php', { const response = await fetch('api/kitchen.php', {
method: 'POST', method: 'POST',
@ -165,23 +184,34 @@ async function updateStatus(orderId, newStatus) {
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
fetchOrders(); // Refresh immediately fetchOrders(); // Refresh immediately
Swal.fire({
icon: 'success',
title: 'Updated!',
text: `Order #${orderId} moved to ${newStatus}`,
timer: 1500,
showConfirmButton: false
});
} else { } else {
alert('Error: ' + (result.error || 'Failed to update')); Swal.fire('Error', result.error || 'Failed to update', 'error');
} }
} catch (error) { } catch (error) {
console.error('Error updating status:', error); console.error('Error updating status:', error);
alert('Failed to connect to server'); Swal.fire('Error', 'Failed to connect to server', 'error');
} }
} }
// Outlet Selector Logic // Outlet Selector Logic
document.getElementById('outlet-selector').addEventListener('change', function() { const outletSelector = document.getElementById('outlet-selector');
window.location.href = '?outlet_id=' + this.value; if (outletSelector) {
}); outletSelector.addEventListener('change', function() {
window.location.href = '?outlet_id=' + this.value;
});
}
// Clock // Clock
setInterval(() => { setInterval(() => {
document.getElementById('clock').textContent = new Date().toLocaleTimeString(); const clock = document.getElementById('clock');
if (clock) clock.textContent = new Date().toLocaleTimeString();
}, 1000); }, 1000);
// Poll orders // Poll orders

View File

@ -43,6 +43,7 @@ foreach ($outlets as $o) {
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>"> <link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style> <style>
body { height: 100vh; overflow: hidden; } /* Fix body for scrolling areas */ body { height: 100vh; overflow: hidden; } /* Fix body for scrolling areas */
.scrollable-y { overflow-y: auto; height: 100%; scrollbar-width: thin; } .scrollable-y { overflow-y: auto; height: 100%; scrollbar-width: thin; }
@ -138,11 +139,11 @@ foreach ($outlets as $o) {
</div> </div>
</div> </div>
<div class="row g-3" id="products-container"> <div class="row row-cols-2 row-cols-lg-5 g-3" id="products-container">
<?php foreach ($all_products as $product): <?php foreach ($all_products as $product):
$has_variants = !empty($variants_by_product[$product['id']]); $has_variants = !empty($variants_by_product[$product['id']]);
?> ?>
<div class="col-6 col-lg-3 col-xl-3 product-item" data-category-id="<?= $product['category_id'] ?>"> <div class="col product-item" data-category-id="<?= $product['category_id'] ?>">
<div class="card h-100 border-0 shadow-sm product-card add-to-cart" <div class="card h-100 border-0 shadow-sm product-card add-to-cart"
data-id="<?= $product['id'] ?>" data-id="<?= $product['id'] ?>"
data-name="<?= htmlspecialchars($product['name']) ?>" data-name="<?= htmlspecialchars($product['name']) ?>"