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;
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 productSearchInput = document.getElementById('product-search');
@ -698,13 +698,12 @@ document.addEventListener('DOMContentLoaded', () => {
customer_id: selectedCustomerId ? selectedCustomerId.value : null,
outlet_id: CURRENT_OUTLET ? CURRENT_OUTLET.id : 1,
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,
items: itemsData,
redeem_loyalty: isLoyaltyRedemption
};
// Prepare receipt data before clearing cart
const receiptData = {
orderId: null,
customer: currentCustomer ? { name: currentCustomer.name, phone: currentCustomer.phone, address: currentCustomer.address || '' } : null,
@ -712,7 +711,7 @@ document.addEventListener('DOMContentLoaded', () => {
name: item.name,
variant_name: item.variant_name,
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_amount: isLoyaltyRedemption ? 0 : ((item.price * item.quantity) * (item.vat_percent / 100))
})),
@ -726,6 +725,12 @@ document.addEventListener('DOMContentLoaded', () => {
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) })
.then(res => res.json())
.then(data => {
@ -734,35 +739,36 @@ document.addEventListener('DOMContentLoaded', () => {
// --- PRINTING LOGIC ---
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:
// 1. No cashier printer is configured
// 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.
const isLocalCashierIp = cashierPrinterIp && /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/.test(cashierPrinterIp);
const isLocalKitchenIp = kitchenPrinterIp && /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/.test(kitchenPrinterIp);
// 1. Browser Cashier Print (if needed)
if (!cashierPrinterIp || isLocalCashierIp) {
printThermalReceipt(receiptData);
} 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');
}
// 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');
}
showToast(`${_t('order_placed')} #${data.order_id}`, 'success');
clearCart();
if (paymentSelectionModal) paymentSelectionModal.hide();
if (clearCustomerBtn) clearCustomerBtn.click();
} else showToast(data.error, 'danger');
} else {
showToast(data.error, 'danger');
// Optional: should we restore the cart if it failed?
// For now, let's keep it cleared but show the error.
}
});
};
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;
@ -774,25 +780,16 @@ document.addEventListener('DOMContentLoaded', () => {
.then(res => res.json())
.then(data => {
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);
if (!isLocalIp) {
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));
};
/**
* 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) {
// Create or get the hidden iframe
let iframe = document.getElementById('print-iframe');
if (!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 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;
@ -927,21 +926,11 @@ document.addEventListener('DOMContentLoaded', () => {
doc.write(html);
doc.close();
// Wait for resources (like logo) to load before printing
iframe.contentWindow.onload = function() {
// Print immediately without waiting for resources
iframe.contentWindow.focus();
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() {
const qrContainer = document.getElementById('rating-qr-container');
const ratingUrl = BASE_URL + '/rate.php';

View File

@ -14,10 +14,15 @@ class PrinterService {
return ['success' => false, 'error' => 'Printer IP is not configured.'];
}
// Determine if IP is local/private. If so, use a very short timeout
// because cloud servers cannot reach local IPs anyway.
// Determine if IP is local/private.
// 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);
$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 {
$fp = @fsockopen($ip, $port, $errno, $errstr, $timeout);
@ -36,7 +41,6 @@ class PrinterService {
/**
* 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";
@ -44,29 +48,34 @@ class PrinterService {
$line = str_repeat("-", 32) . "\n";
$out = "";
$out .= $esc . "@"; // Initialize printer
$out .= $esc . "!" . "\x38"; // Double height and width
$out .= $esc . "a" . "\x01"; // Center align
$out .= $company['company_name'] . "\n";
$out .= ($company['company_name'] ?? 'Restaurant') . "\n";
$out .= $esc . "!" . "\x00"; // Reset
$out .= $company['address'] . "\n";
$out .= $company['phone'] . "\n\n";
$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 .= "Date: " . ($order['created_at'] ?? date('Y-m-d H:i:s')) . "\n";
$out .= $line;
foreach ($items as $item) {
$name = substr($item['name'], 0, 20);
$qty = $item['quantity'] . "x";
$price = number_format($item['price'], 2);
$name = substr(($item['product_name'] ?? $item['name'] ?? 'Item'), 0, 20);
$qty = ($item['quantity'] ?? 1) . "x";
$price = number_format((float)($item['unit_price'] ?? $item['price'] ?? 0), 2);
$out .= sprintf("% -20s %3s %7s\n", $name, $qty, $price);
if (!empty($item['variant_name'])) {
$out .= " (" . $item['variant_name'] . ")\n";
}
}
$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 .= $esc . "m"; // Cut paper (optional, depending on printer)
$out .= $esc . "m"; // Cut paper
return $out;
}