diff --git a/admin/loyalty.php b/admin/loyalty.php index 477502e..93a9c65 100644 --- a/admin/loyalty.php +++ b/admin/loyalty.php @@ -2,64 +2,187 @@ require_once __DIR__ . '/../db/config.php'; $pdo = db(); -$query = "SELECT * FROM loyalty_customers ORDER BY points DESC"; -$customers_pagination = paginate_query($pdo, $query); -$customers = $customers_pagination['data']; +// Handle Settings Update +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_settings'])) { + $points_per_order = intval($_POST['points_per_order']); + $points_for_free_meal = intval($_POST['points_for_free_meal']); + + $stmt = $pdo->prepare("UPDATE loyalty_settings SET points_per_order = ?, points_for_free_meal = ? WHERE id = 1"); + $stmt->execute([$points_per_order, $points_for_free_meal]); + + $success_msg = "Loyalty settings updated successfully!"; +} + +// Fetch Settings +$stmt = $pdo->query("SELECT * FROM loyalty_settings WHERE id = 1"); +$settings = $stmt->fetch(PDO::FETCH_ASSOC); + +if (!$settings) { + // Default fallback if migration failed or empty + $settings = ['points_per_order' => 10, 'points_for_free_meal' => 70]; +} + +// Fetch Customers with Points +$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1; +$limit = 20; +$offset = ($page - 1) * $limit; + +$count_stmt = $pdo->query("SELECT COUNT(*) FROM customers WHERE points > 0"); +$total_customers = $count_stmt->fetchColumn(); +$total_pages = ceil($total_customers / $limit); + +$query = "SELECT * FROM customers WHERE points > 0 ORDER BY points DESC LIMIT $limit OFFSET $offset"; +$stmt = $pdo->query($query); +$customers = $stmt->fetchAll(PDO::FETCH_ASSOC); include 'includes/header.php'; ?>

Loyalty Program

-
-
-
- -
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - -
IDNameEmailPoints BalanceJoined
# - - pts - -
No loyalty members yet.
-
- -
- + + + + +
+
+
+
+
Current Configuration
+
+
+ Points per Order + pts +
+
+ Free Meal Threshold + pts +
+
+
- \ No newline at end of file +
+
+
Loyalty Members
+
+
+ + + + + + + + + + + + + = $settings['points_for_free_meal']; + ?> + + + + + + + + + + + + + + +
CustomerContactPoints BalanceStatusJoined
+
+
+
+
+
+
+ + pts + +
+
+
+
+
+ + + Eligible for Free Meal + + + + pts to go + + +
No active loyalty members found.
+
+ + + 1): ?> + + +
+ + + + + diff --git a/admin/orders.php b/admin/orders.php index 05dac41..f960556 100644 --- a/admin/orders.php +++ b/admin/orders.php @@ -9,16 +9,55 @@ if (isset($_POST['action']) && $_POST['action'] === 'update_status') { $new_status = $_POST['status']; $stmt = $pdo->prepare("UPDATE orders SET status = ? WHERE id = ?"); $stmt->execute([$new_status, $order_id]); - header("Location: orders.php"); + header("Location: orders.php?" . http_build_query($_GET)); // Keep filters exit; } -$query = "SELECT o.*, +// Fetch Outlets for Filter +$outlets = $pdo->query("SELECT id, name FROM outlets ORDER BY name")->fetchAll(PDO::FETCH_ASSOC); + +// Build Query with Filters +$params = []; +$where = []; + +// Filter: Outlet +if (!empty($_GET['outlet_id'])) { + $where[] = "o.outlet_id = :outlet_id"; + $params[':outlet_id'] = $_GET['outlet_id']; +} + +// Filter: Date Range +if (!empty($_GET['start_date'])) { + $where[] = "DATE(o.created_at) >= :start_date"; + $params[':start_date'] = $_GET['start_date']; +} +if (!empty($_GET['end_date'])) { + $where[] = "DATE(o.created_at) <= :end_date"; + $params[':end_date'] = $_GET['end_date']; +} + +// Filter: Search (Order No) +if (!empty($_GET['search'])) { + // Exact match for ID usually, but LIKE might be more user friendly if they type partial? + // "search by order no" usually implies ID. Let's stick to ID or simple LIKE. + // If numeric, assume ID. + if (is_numeric($_GET['search'])) { + $where[] = "o.id = :search"; + $params[':search'] = $_GET['search']; + } +} + +$where_clause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; + +// Changed alias 'out' to 'ot' to avoid reserved keyword conflict +$query = "SELECT o.*, ot.name as outlet_name, (SELECT GROUP_CONCAT(CONCAT(p.name, ' x', oi.quantity) SEPARATOR ', ') FROM order_items oi JOIN products p ON oi.product_id = p.id WHERE oi.order_id = o.id) as items_summary FROM orders o + LEFT JOIN outlets ot ON o.outlet_id = ot.id + $where_clause ORDER BY o.created_at DESC"; -$orders_pagination = paginate_query($pdo, $query); +$orders_pagination = paginate_query($pdo, $query, $params); $orders = $orders_pagination['data']; include 'includes/header.php'; @@ -31,6 +70,47 @@ include 'includes/header.php';
+ +
+
+
+
+ + +
+
+ +
+ + - + +
+
+
+ + +
+
+
+ + + + +
+
+
+
+
+
@@ -42,6 +122,8 @@ include 'includes/header.php'; ID + Outlet + Customer Type Source Items @@ -55,6 +137,22 @@ include 'includes/header.php'; # + + + + + + + + +
+ + + + + Guest + + - + +
+
+
+ + `; + 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'; + } + + // Initialize Payment Methods + renderPaymentMethods(); + + // --- Checkout Flow --- checkoutBtn.addEventListener('click', () => { if (cart.length === 0) return; @@ -382,6 +509,13 @@ document.addEventListener('DOMContentLoaded', () => { return; } + // Open Payment Modal instead of direct submission + paymentSelectionModal.show(); + }); + + window.processOrder = function(paymentTypeId, paymentTypeName) { + const orderTypeInput = document.querySelector('input[name="order_type"]:checked'); + const orderType = orderTypeInput ? orderTypeInput.value : 'dine-in'; 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); @@ -392,8 +526,10 @@ document.addEventListener('DOMContentLoaded', () => { 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, // Send flag items: cart.map(item => ({ product_id: item.id, quantity: item.quantity, @@ -402,8 +538,9 @@ document.addEventListener('DOMContentLoaded', () => { })) }; - checkoutBtn.disabled = true; - checkoutBtn.innerHTML = ' Processing...'; + // Disable all payment buttons + const btns = paymentMethodsContainer.querySelectorAll('button'); + btns.forEach(b => b.disabled = true); fetch('api/order.php', { method: 'POST', @@ -412,12 +549,27 @@ document.addEventListener('DOMContentLoaded', () => { }) .then(res => res.json()) .then(data => { - checkoutBtn.disabled = false; - checkoutBtn.innerHTML = 'Place Order '; + btns.forEach(b => b.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; + isLoyaltyRedemption = false; // Reset updateCart(); if (clearCustomerBtn) clearCustomerBtn.click(); showToast(`Order #${data.order_id} placed!`, 'success'); @@ -426,11 +578,11 @@ document.addEventListener('DOMContentLoaded', () => { } }) .catch(err => { - checkoutBtn.disabled = false; - checkoutBtn.innerHTML = 'Place Order'; + btns.forEach(b => b.disabled = false); + paymentSelectionModal.hide(); showToast('Network Error', 'danger'); }); - }); + }; function showToast(msg, type = 'primary') { const toastContainer = document.getElementById('toast-container'); @@ -450,6 +602,174 @@ document.addEventListener('DOMContentLoaded', () => { el.addEventListener('hidden.bs.toast', () => el.remove()); } + 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 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'); + }); + }); + } }); \ No newline at end of file diff --git a/db/migrations/002_add_customer_id_to_orders.sql b/db/migrations/002_add_customer_id_to_orders.sql new file mode 100644 index 0000000..fff4777 --- /dev/null +++ b/db/migrations/002_add_customer_id_to_orders.sql @@ -0,0 +1,44 @@ +-- Create customers table if it doesn't exist +CREATE TABLE IF NOT EXISTS customers ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + phone VARCHAR(20) UNIQUE, + email VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Add customer_id to orders table +SET @dbname = DATABASE(); +SET @tablename = "orders"; +SET @columnname = "customer_id"; +SET @preparedStatement = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE + (table_name = @tablename) + AND (table_schema = @dbname) + AND (column_name = @columnname) + ) > 0, + "SELECT 1", + "ALTER TABLE orders ADD COLUMN customer_id INT DEFAULT NULL AFTER order_type;" +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; + +-- Add foreign key if it doesn't exist +SET @constraintname = "fk_orders_customer"; +SET @preparedStatement = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS + WHERE + (table_name = @tablename) + AND (table_schema = @dbname) + AND (constraint_name = @constraintname) + ) > 0, + "SELECT 1", + "ALTER TABLE orders ADD CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE SET NULL;" +)); +PREPARE addConstraint FROM @preparedStatement; +EXECUTE addConstraint; +DEALLOCATE PREPARE addConstraint; diff --git a/db/migrations/003_add_payment_type_to_orders.sql b/db/migrations/003_add_payment_type_to_orders.sql new file mode 100644 index 0000000..590d7ac --- /dev/null +++ b/db/migrations/003_add_payment_type_to_orders.sql @@ -0,0 +1,15 @@ +-- Add payment_type_id to orders table +ALTER TABLE orders ADD COLUMN payment_type_id INT DEFAULT NULL; + +-- Seed default payment types if table is empty +INSERT INTO payment_types (name, type, is_active) +SELECT * FROM (SELECT 'Cash', 'cash', 1) AS tmp +WHERE NOT EXISTS ( + SELECT name FROM payment_types WHERE name = 'Cash' +) LIMIT 1; + +INSERT INTO payment_types (name, type, is_active) +SELECT * FROM (SELECT 'Credit Card', 'card', 1) AS tmp +WHERE NOT EXISTS ( + SELECT name FROM payment_types WHERE name = 'Credit Card' +) LIMIT 1; diff --git a/db/migrations/004_seed_payment_types.sql b/db/migrations/004_seed_payment_types.sql new file mode 100644 index 0000000..df96ca0 --- /dev/null +++ b/db/migrations/004_seed_payment_types.sql @@ -0,0 +1,11 @@ +INSERT INTO payment_types (name, type, is_active) +SELECT * FROM (SELECT 'Cash' as n, 'cash' as t, 1 as a) AS tmp +WHERE NOT EXISTS ( + SELECT name FROM payment_types WHERE name = 'Cash' +) LIMIT 1; + +INSERT INTO payment_types (name, type, is_active) +SELECT * FROM (SELECT 'Credit Card' as n, 'card' as t, 1 as a) AS tmp +WHERE NOT EXISTS ( + SELECT name FROM payment_types WHERE name = 'Credit Card' +) LIMIT 1; \ No newline at end of file diff --git a/db/migrations/005_loyalty_schema.sql b/db/migrations/005_loyalty_schema.sql new file mode 100644 index 0000000..a1c79fd --- /dev/null +++ b/db/migrations/005_loyalty_schema.sql @@ -0,0 +1,28 @@ +-- Add points column to customers table if it doesn't exist +SET @dbname = DATABASE(); +SET @tablename = "customers"; +SET @columnname = "points"; +SET @preparedStatement = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE + (table_name = @tablename) + AND (table_schema = @dbname) + AND (column_name = @columnname) + ) > 0, + "SELECT 1", + "ALTER TABLE customers ADD COLUMN points INT DEFAULT 0;" +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; + +-- Create loyalty_settings table +CREATE TABLE IF NOT EXISTS loyalty_settings ( + id INT PRIMARY KEY, + points_per_order INT DEFAULT 10, + points_for_free_meal INT DEFAULT 70 +); + +-- Seed default settings +INSERT IGNORE INTO loyalty_settings (id, points_per_order, points_for_free_meal) VALUES (1, 10, 70); diff --git a/db/migrations/006_integration_settings.sql b/db/migrations/006_integration_settings.sql new file mode 100644 index 0000000..edab36e --- /dev/null +++ b/db/migrations/006_integration_settings.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS integration_settings ( + id INT AUTO_INCREMENT PRIMARY KEY, + provider VARCHAR(50) NOT NULL, + setting_key VARCHAR(100) NOT NULL, + setting_value TEXT, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY unique_provider_key (provider, setting_key) +); diff --git a/kitchen.php b/kitchen.php index 6868bb4..45efe44 100644 --- a/kitchen.php +++ b/kitchen.php @@ -1,3 +1,9 @@ +query("SELECT * FROM outlets ORDER BY name")->fetchAll(PDO::FETCH_ASSOC); +$current_outlet_id = isset($_GET['outlet_id']) ? (int)$_GET['outlet_id'] : 1; +?> @@ -50,7 +56,14 @@

Kitchen Display

-
+
+ Back to Home
@@ -65,9 +78,11 @@