diff --git a/admin/outlets.php b/admin/outlets.php index 7a14704..2142846 100644 --- a/admin/outlets.php +++ b/admin/outlets.php @@ -13,6 +13,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) { $name = trim($_POST['name']); $name_ar = trim($_POST['name_ar'] ?? ''); $address = trim($_POST['address']); + $cashier_printer_ip = trim($_POST['cashier_printer_ip'] ?? ''); + $kitchen_printer_ip = trim($_POST['kitchen_printer_ip'] ?? ''); $id = isset($_POST['id']) ? (int)$_POST['id'] : null; if (empty($name)) { @@ -23,16 +25,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) { if (!has_permission('outlets_add')) { $message = '
Access Denied.
'; } else { - $stmt = $pdo->prepare("UPDATE outlets SET name = ?, name_ar = ?, address = ? WHERE id = ?"); - $stmt->execute([$name, $name_ar, $address, $id]); + $stmt = $pdo->prepare("UPDATE outlets SET name = ?, name_ar = ?, address = ?, cashier_printer_ip = ?, kitchen_printer_ip = ? WHERE id = ?"); + $stmt->execute([$name, $name_ar, $address, $cashier_printer_ip, $kitchen_printer_ip, $id]); $message = '
Outlet updated successfully!
'; } } elseif ($action === 'add_outlet') { if (!has_permission('outlets_add')) { $message = '
Access Denied.
'; } else { - $stmt = $pdo->prepare("INSERT INTO outlets (name, name_ar, address) VALUES (?, ?, ?)"); - $stmt->execute([$name, $name_ar, $address]); + $stmt = $pdo->prepare("INSERT INTO outlets (name, name_ar, address, cashier_printer_ip, kitchen_printer_ip) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$name, $name_ar, $address, $cashier_printer_ip, $kitchen_printer_ip]); $message = '
Outlet created successfully!
'; } } @@ -93,8 +95,8 @@ include 'includes/header.php'; ID Name - Arabic Name Address + Printers (Cashier / Kitchen) Actions @@ -102,9 +104,19 @@ include 'includes/header.php'; # - - + +
+ + + +
+ +
+
+ +
+ @@ -189,6 +211,8 @@ function prepareEditForm(outlet) { document.getElementById('outletName').value = outlet.name || ''; document.getElementById('outletNameAr').value = outlet.name_ar || ''; document.getElementById('outletAddress').value = outlet.address || ''; + document.getElementById('outletCashierIp').value = outlet.cashier_printer_ip || ''; + document.getElementById('outletKitchenIp').value = outlet.kitchen_printer_ip || ''; } document.getElementById('btnTranslate').addEventListener('click', function() { @@ -233,4 +257,4 @@ document.getElementById('btnTranslate').addEventListener('click', function() { - + \ No newline at end of file diff --git a/api/print.php b/api/print.php new file mode 100644 index 0000000..e525cf0 --- /dev/null +++ b/api/print.php @@ -0,0 +1,60 @@ + false, 'error' => 'Order ID is required']); + exit; +} + +try { + // Fetch order details with outlet info + $stmt = $pdo->prepare(" + SELECT o.*, out.cashier_printer_ip, out.kitchen_printer_ip + FROM orders o + JOIN outlets out ON o.outlet_id = out.id + WHERE o.id = ? + "); + $stmt->execute([$order_id]); + $order = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$order) { + echo json_encode(['success' => false, 'error' => 'Order not found']); + exit; + } + + // Fetch items + $stmt = $pdo->prepare("SELECT * FROM order_items WHERE order_id = ?"); + $stmt->execute([$order_id]); + $items = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Fetch company settings + $company = get_company_settings(); + + // Determine target IP + $printer_ip = ($type === 'kitchen') ? $order['kitchen_printer_ip'] : $order['cashier_printer_ip']; + + if (empty($printer_ip)) { + echo json_encode(['success' => false, 'error' => "No $type printer IP configured for this outlet"]); + exit; + } + + // Generate ESC/POS data + $data = PrinterService::formatReceipt($order, $items, $company); + + // Send to printer + $result = PrinterService::sendToNetworkPrinter($printer_ip, $data); + + echo json_encode($result); + +} catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/assets/js/main.js b/assets/js/main.js index f0fd0a6..e79c683 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -589,6 +589,7 @@ document.addEventListener('DOMContentLoaded', () => { if (cartVatAmount) cartVatAmount.innerText = formatCurrency(0); // Always show VAT row even if 0 if (cartVatRow) cartVatRow.classList.remove('d-none'); + if (cartVatInput) cartVatInput.value = totalVat.toFixed(3); if (cartTotalPrice) cartTotalPrice.innerText = formatCurrency(0); if (quickOrderBtn) { quickOrderBtn.disabled = true; quickOrderBtn.innerText = _t('quick_pay'); } if (placeOrderBtn) { placeOrderBtn.disabled = true; placeOrderBtn.innerText = _t('save_bill'); } @@ -734,6 +735,10 @@ document.addEventListener('DOMContentLoaded', () => { // Show receipt for both Quick Pay and Place Order printThermalReceipt(receiptData); + // Trigger network printing + triggerNetworkPrint(data.order_id, 'cashier'); + triggerNetworkPrint(data.order_id, 'kitchen'); + showToast(`${_t('order_placed')} #${data.order_id}`, 'success'); clearCart(); if (paymentSelectionModal) paymentSelectionModal.hide(); @@ -742,6 +747,28 @@ document.addEventListener('DOMContentLoaded', () => { }); }; + window.triggerNetworkPrint = function(orderId, type) { + if (!orderId) return; + + // Check if printer IP is configured for this outlet + const printerIp = (type === 'kitchen') ? CURRENT_OUTLET.kitchen_printer_ip : CURRENT_OUTLET.cashier_printer_ip; + if (!printerIp) return; + + fetch('api/print.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ order_id: orderId, type: type }) + }) + .then(res => res.json()) + .then(data => { + if (!data.success) { + console.error(`Network Print Error (${type}):`, data.error); + // Optionally show a toast for errors, but might be too noisy + } + }) + .catch(err => console.error(`Network Print Fetch Error (${type}):`, err)); + }; + window.printThermalReceipt = function(data) { const width = 450; const height = 800; @@ -890,4 +917,4 @@ document.addEventListener('DOMContentLoaded', () => { const modal = new bootstrap.Modal(document.getElementById('qrRatingModal')); modal.show(); }; -}); \ No newline at end of file +}); diff --git a/db/migrations/043_add_printer_ips_to_outlets.sql b/db/migrations/043_add_printer_ips_to_outlets.sql new file mode 100644 index 0000000..4133d36 --- /dev/null +++ b/db/migrations/043_add_printer_ips_to_outlets.sql @@ -0,0 +1,3 @@ +-- Add printer IP columns to outlets table +ALTER TABLE outlets ADD COLUMN cashier_printer_ip VARCHAR(45) DEFAULT NULL; +ALTER TABLE outlets ADD COLUMN kitchen_printer_ip VARCHAR(45) DEFAULT NULL; diff --git a/includes/PrinterService.php b/includes/PrinterService.php new file mode 100644 index 0000000..135091f --- /dev/null +++ b/includes/PrinterService.php @@ -0,0 +1,68 @@ + false, 'error' => 'Printer IP is not configured.']; + } + + try { + $fp = @fsockopen($ip, $port, $errno, $errstr, 5); + if (!$fp) { + return ['success' => false, 'error' => "Could not connect to printer at $ip:$port. Error: $errstr ($errno)"]; + } + + fwrite($fp, $data); + fclose($fp); + + return ['success' => true]; + } catch (Exception $e) { + return ['success' => false, 'error' => 'Exception: ' . $e->getMessage()]; + } + } + + /** + * Generate basic ESC/POS receipt content. + * This is a simplified version. For full features, an ESC/POS library is recommended. + */ + public static function formatReceipt($order, $items, $company) { + $esc = "\x1b"; + $gs = "\x1d"; + $line = str_repeat("-", 32) . "\n"; + + $out = ""; + $out .= $esc . "!" . "\x38"; // Double height and width + $out .= $esc . "a" . "\x01"; // Center align + $out .= $company['company_name'] . "\n"; + $out .= $esc . "!" . "\x00"; // Reset + $out .= $company['address'] . "\n"; + $out .= $company['phone'] . "\n\n"; + + $out .= $esc . "a" . "\x00"; // Left align + $out .= "Order ID: #" . $order['id'] . "\n"; + $out .= "Date: " . $order['created_at'] . "\n"; + $out .= $line; + + foreach ($items as $item) { + $name = substr($item['name'], 0, 20); + $qty = $item['quantity'] . "x"; + $price = number_format($item['price'], 2); + $out .= sprintf("% -20s %3s %7s\n", $name, $qty, $price); + } + + $out .= $line; + $out .= sprintf("% -20s %11s\n", "TOTAL", number_format($order['total_amount'], 2)); + $out .= "\n\n\n\n"; + $out .= $esc . "m"; // Cut paper (optional, depending on printer) + + return $out; + } +} diff --git a/pos.php b/pos.php index 0071038..2e876be 100644 --- a/pos.php +++ b/pos.php @@ -61,13 +61,16 @@ $table_id = $_GET['table'] ?? '1'; // Default table $settings = get_company_settings(); $order_type = $_GET['order_type'] ?? 'takeaway'; -$current_outlet_name = 'Unknown Outlet'; +$current_outlet = null; foreach ($outlets as $o) { if ($o['id'] == $outlet_id) { - $current_outlet_name = $o['name']; + $current_outlet = $o; break; } } +if (!$current_outlet && count($outlets) > 0) { + $current_outlet = $outlets[0]; +} // Fetch Loyalty Settings $loyalty_stmt = $pdo->query("SELECT * FROM loyalty_settings WHERE id = 1"); @@ -462,7 +465,7 @@ $vat_rate = (float)($settings['vat_rate'] ?? 0); const PRODUCT_VARIANTS = ; const PAYMENT_TYPES = ; const BASE_URL = ''; - const CURRENT_OUTLET = { id: , name: '' }; + const CURRENT_OUTLET = ; const CURRENT_USER = { id: , name: '' }; const LOYALTY_SETTINGS = ; const LANG = 'en';