improving printing 2

This commit is contained in:
Flatlogic Bot 2026-02-28 13:25:47 +00:00
parent 5deb44b9f8
commit b0479e299c
2 changed files with 54 additions and 56 deletions

View File

@ -89,7 +89,7 @@ document.addEventListener('DOMContentLoaded', () => {
let currentCustomer = null; let currentCustomer = null;
const paymentModalEl = document.getElementById('paymentSelectionModal'); const paymentModalEl = document.getElementById('paymentSelectionModal');
const paymentSelectionModal = paymentModalEl ? new bootstrap.Modal(paymentModalEl) : null; const paymentSelectionModal = paymentModalEl ? new bootstrap.Modal(paymentSelectionModal) : null;
const paymentMethodsContainer = document.getElementById('payment-methods-container'); const paymentMethodsContainer = document.getElementById('payment-methods-container');
const productSearchInput = document.getElementById('product-search'); const productSearchInput = document.getElementById('product-search');
@ -698,13 +698,12 @@ document.addEventListener('DOMContentLoaded', () => {
customer_id: selectedCustomerId ? selectedCustomerId.value : null, customer_id: selectedCustomerId ? selectedCustomerId.value : null,
outlet_id: CURRENT_OUTLET ? CURRENT_OUTLET.id : 1, outlet_id: CURRENT_OUTLET ? CURRENT_OUTLET.id : 1,
payment_type_id: paymentTypeId, payment_type_id: paymentTypeId,
total_amount: isLoyaltyRedemption ? 0 : (subtotal + totalVat), // If loyalty redeemed, total is 0 for this transaction total_amount: isLoyaltyRedemption ? 0 : (subtotal + totalVat),
vat: isLoyaltyRedemption ? 0 : totalVat, vat: isLoyaltyRedemption ? 0 : totalVat,
items: itemsData, items: itemsData,
redeem_loyalty: isLoyaltyRedemption redeem_loyalty: isLoyaltyRedemption
}; };
// Prepare receipt data before clearing cart
const receiptData = { const receiptData = {
orderId: null, orderId: null,
customer: currentCustomer ? { name: currentCustomer.name, phone: currentCustomer.phone, address: currentCustomer.address || '' } : null, customer: currentCustomer ? { name: currentCustomer.name, phone: currentCustomer.phone, address: currentCustomer.address || '' } : null,
@ -712,7 +711,7 @@ document.addEventListener('DOMContentLoaded', () => {
name: item.name, name: item.name,
variant_name: item.variant_name, variant_name: item.variant_name,
quantity: item.quantity, quantity: item.quantity,
price: isLoyaltyRedemption ? 0 : item.price, // If loyalty redeemed, price is 0 for this transaction price: isLoyaltyRedemption ? 0 : item.price,
vat_percent: isLoyaltyRedemption ? 0 : item.vat_percent, vat_percent: isLoyaltyRedemption ? 0 : item.vat_percent,
vat_amount: isLoyaltyRedemption ? 0 : ((item.price * item.quantity) * (item.vat_percent / 100)) vat_amount: isLoyaltyRedemption ? 0 : ((item.price * item.quantity) * (item.vat_percent / 100))
})), })),
@ -726,6 +725,12 @@ document.addEventListener('DOMContentLoaded', () => {
loyaltyRedeemed: isLoyaltyRedemption loyaltyRedeemed: isLoyaltyRedemption
}; };
// Clear UI immediately for responsiveness
const tempOrderId = currentOrderId;
clearCart();
if (paymentSelectionModal) paymentSelectionModal.hide();
if (clearCustomerBtn) clearCustomerBtn.click();
fetch('api/order.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(orderData) }) fetch('api/order.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(orderData) })
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
@ -734,35 +739,36 @@ document.addEventListener('DOMContentLoaded', () => {
// --- PRINTING LOGIC --- // --- PRINTING LOGIC ---
const cashierPrinterIp = CURRENT_OUTLET.cashier_printer_ip; const cashierPrinterIp = CURRENT_OUTLET.cashier_printer_ip;
const isLocalCashierIp = cashierPrinterIp && /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/.test(cashierPrinterIp); const kitchenPrinterIp = CURRENT_OUTLET.kitchen_printer_ip;
// Show browser receipt ONLY IF: const isLocalCashierIp = cashierPrinterIp && /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/.test(cashierPrinterIp);
// 1. No cashier printer is configured const isLocalKitchenIp = kitchenPrinterIp && /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/.test(kitchenPrinterIp);
// 2. OR the configured cashier printer is a local IP (which we can't reach from cloud)
// This prevents duplicate printing when a valid network printer is used. // 1. Browser Cashier Print (if needed)
if (!cashierPrinterIp || isLocalCashierIp) { if (!cashierPrinterIp || isLocalCashierIp) {
printThermalReceipt(receiptData); printThermalReceipt(receiptData);
} else { } else {
// Only try network printing if it's NOT a local IP
// (prevents useless 1s timeout for cashier print if we already browser-printed)
triggerNetworkPrint(data.order_id, 'cashier'); triggerNetworkPrint(data.order_id, 'cashier');
} }
// Kitchen is usually always a network printer, so we always try it // 2. Kitchen Network Print (ONLY if NOT local IP)
// If it's local, server can't reach it anyway, so skip the fetch to api/print.php
if (kitchenPrinterIp && !isLocalKitchenIp) {
triggerNetworkPrint(data.order_id, 'kitchen'); triggerNetworkPrint(data.order_id, 'kitchen');
}
showToast(`${_t('order_placed')} #${data.order_id}`, 'success'); showToast(`${_t('order_placed')} #${data.order_id}`, 'success');
clearCart(); } else {
if (paymentSelectionModal) paymentSelectionModal.hide(); showToast(data.error, 'danger');
if (clearCustomerBtn) clearCustomerBtn.click(); // Optional: should we restore the cart if it failed?
} else showToast(data.error, 'danger'); // For now, let's keep it cleared but show the error.
}
}); });
}; };
window.triggerNetworkPrint = function(orderId, type) { window.triggerNetworkPrint = function(orderId, type) {
if (!orderId) return; 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; const printerIp = (type === 'kitchen') ? CURRENT_OUTLET.kitchen_printer_ip : CURRENT_OUTLET.cashier_printer_ip;
if (!printerIp) return; if (!printerIp) return;
@ -774,25 +780,16 @@ document.addEventListener('DOMContentLoaded', () => {
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
if (!data.success) { if (!data.success) {
// Skip toast for local IPs as we know they are unreachable from cloud
const isLocalIp = /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/.test(printerIp); const isLocalIp = /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/.test(printerIp);
if (!isLocalIp) { if (!isLocalIp) {
showToast(`Printer Error (${type}): ${data.error}`, 'warning'); showToast(`Printer Error (${type}): ${data.error}`, 'warning');
} else {
console.warn(`Printer Error (${type}) for local IP ${printerIp}: ${data.error}`);
} }
} }
}) })
.catch(err => console.error(`Network Print Fetch Error (${type}):`, err)); .catch(err => console.error(`Network Print Fetch Error (${type}):`, err));
}; };
/**
* Prints a thermal receipt using a hidden iframe for a smoother experience.
* To achieve completely silent printing (Direct Print), run Chrome with:
* chrome.exe --kiosk-printing --kiosk
*/
window.printThermalReceipt = function(data) { window.printThermalReceipt = function(data) {
// Create or get the hidden iframe
let iframe = document.getElementById('print-iframe'); let iframe = document.getElementById('print-iframe');
if (!iframe) { if (!iframe) {
iframe = document.createElement('iframe'); iframe = document.createElement('iframe');
@ -849,7 +846,9 @@ document.addEventListener('DOMContentLoaded', () => {
const loyaltyHtml = data.loyaltyRedeemed ? `<div style="color: #d63384; font-weight: bold; margin: 5px 0; text-align: center;">* Loyalty Reward Applied *</div>` : ''; const loyaltyHtml = data.loyaltyRedeemed ? `<div style="color: #d63384; font-weight: bold; margin: 5px 0; text-align: center;">* Loyalty Reward Applied *</div>` : '';
const logoHtml = settings.logo_url ? `<img src="${BASE_URL}${settings.logo_url}" style="max-height: 80px; max-width: 150px; margin-bottom: 10px;">` : ''; // We skip logo in receipt for absolute speed unless it's already cached.
// If users really want the logo, we can re-enable it.
const logoHtml = ''; // settings.logo_url ? `<img src="${BASE_URL}${settings.logo_url}" style="max-height: 80px; max-width: 150px; margin-bottom: 10px;">` : '';
const vatRate = settings.vat_rate || 0; const vatRate = settings.vat_rate || 0;
@ -927,21 +926,11 @@ document.addEventListener('DOMContentLoaded', () => {
doc.write(html); doc.write(html);
doc.close(); doc.close();
// Wait for resources (like logo) to load before printing // Print immediately without waiting for resources
iframe.contentWindow.onload = function() {
iframe.contentWindow.focus(); iframe.contentWindow.focus();
iframe.contentWindow.print(); iframe.contentWindow.print();
}; };
// Fallback if onload doesn't fire correctly
setTimeout(() => {
if (doc.readyState === 'complete') {
iframe.contentWindow.focus();
iframe.contentWindow.print();
}
}, 1000);
};
window.openRatingQRModal = function() { window.openRatingQRModal = function() {
const qrContainer = document.getElementById('rating-qr-container'); const qrContainer = document.getElementById('rating-qr-container');
const ratingUrl = BASE_URL + '/rate.php'; const ratingUrl = BASE_URL + '/rate.php';

View File

@ -14,10 +14,15 @@ class PrinterService {
return ['success' => false, 'error' => 'Printer IP is not configured.']; return ['success' => false, 'error' => 'Printer IP is not configured.'];
} }
// Determine if IP is local/private. If so, use a very short timeout // Determine if IP is local/private.
// because cloud servers cannot reach local IPs anyway. // Cloud servers cannot reach local network IPs (e.g., 192.168.x.x).
// If it's a local IP, we return immediately to avoid the 1-second fsockopen timeout.
$isLocalIp = preg_match('/^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/', $ip); $isLocalIp = preg_match('/^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/', $ip);
$timeout = $isLocalIp ? 1 : 5; // 1 second for local, 5 for public if ($isLocalIp) {
return ['success' => false, 'error' => "Cannot connect to local network IP $ip from cloud. Please use a public IP or a local print proxy."];
}
$timeout = 5; // 5 seconds for public IPs
try { try {
$fp = @fsockopen($ip, $port, $errno, $errstr, $timeout); $fp = @fsockopen($ip, $port, $errno, $errstr, $timeout);
@ -36,7 +41,6 @@ class PrinterService {
/** /**
* Generate basic ESC/POS receipt content. * 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) { public static function formatReceipt($order, $items, $company) {
$esc = "\x1b"; $esc = "\x1b";
@ -44,29 +48,34 @@ class PrinterService {
$line = str_repeat("-", 32) . "\n"; $line = str_repeat("-", 32) . "\n";
$out = ""; $out = "";
$out .= $esc . "@"; // Initialize printer
$out .= $esc . "!" . "\x38"; // Double height and width $out .= $esc . "!" . "\x38"; // Double height and width
$out .= $esc . "a" . "\x01"; // Center align $out .= $esc . "a" . "\x01"; // Center align
$out .= $company['company_name'] . "\n"; $out .= ($company['company_name'] ?? 'Restaurant') . "\n";
$out .= $esc . "!" . "\x00"; // Reset $out .= $esc . "!" . "\x00"; // Reset
$out .= $company['address'] . "\n"; $out .= ($company['address'] ?? '') . "\n";
$out .= $company['phone'] . "\n\n"; $out .= ($company['phone'] ?? '') . "\n\n";
$out .= $esc . "a" . "\x00"; // Left align $out .= $esc . "a" . "\x00"; // Left align
$out .= "Order ID: #" . $order['id'] . "\n"; $out .= "Order ID: #" . $order['id'] . "\n";
$out .= "Date: " . $order['created_at'] . "\n"; $out .= "Date: " . ($order['created_at'] ?? date('Y-m-d H:i:s')) . "\n";
$out .= $line; $out .= $line;
foreach ($items as $item) { foreach ($items as $item) {
$name = substr($item['name'], 0, 20); $name = substr(($item['product_name'] ?? $item['name'] ?? 'Item'), 0, 20);
$qty = $item['quantity'] . "x"; $qty = ($item['quantity'] ?? 1) . "x";
$price = number_format($item['price'], 2); $price = number_format((float)($item['unit_price'] ?? $item['price'] ?? 0), 2);
$out .= sprintf("% -20s %3s %7s\n", $name, $qty, $price); $out .= sprintf("% -20s %3s %7s\n", $name, $qty, $price);
if (!empty($item['variant_name'])) {
$out .= " (" . $item['variant_name'] . ")\n";
}
} }
$out .= $line; $out .= $line;
$out .= sprintf("% -20s %11s\n", "TOTAL", number_format($order['total_amount'], 2)); $out .= sprintf("% -20s %11s\n", "TOTAL", number_format((float)($order['total_amount'] ?? 0), 2));
$out .= "\n\n\n\n"; $out .= "\n\n\n\n";
$out .= $esc . "m"; // Cut paper (optional, depending on printer) $out .= $esc . "m"; // Cut paper
return $out; return $out;
} }