update pos

This commit is contained in:
Flatlogic Bot 2026-05-07 09:39:42 +00:00
parent 8c0d2453dd
commit 40fd435ffd
3 changed files with 243 additions and 37 deletions

View File

@ -8332,6 +8332,11 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
const paid = this.payments.reduce((sum, p) => sum + p.amount, 0); const paid = this.payments.reduce((sum, p) => sum + p.amount, 0);
return total - paid; return total - paid;
}, },
setCompleteOrderButtonsDisabled(disabled) {
document.querySelectorAll('.pos-complete-order-btn').forEach(button => {
button.disabled = !!disabled;
});
},
renderPayments() { renderPayments() {
const container = document.getElementById('paymentList'); const container = document.getElementById('paymentList');
const methodLabels = { const methodLabels = {
@ -8379,14 +8384,14 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
if (remaining <= 0.0001 || currentInput >= remaining - 0.0001) { if (remaining <= 0.0001 || currentInput >= remaining - 0.0001) {
display.classList.remove('text-danger'); display.classList.remove('text-danger');
display.classList.add('text-success'); display.classList.add('text-success');
document.getElementById('confirmPaymentBtn').disabled = false; this.setCompleteOrderButtonsDisabled(false);
} else { } else {
display.classList.remove('text-success'); display.classList.remove('text-success');
display.classList.add('text-danger'); display.classList.add('text-danger');
document.getElementById('confirmPaymentBtn').disabled = true; this.setCompleteOrderButtonsDisabled(true);
} }
}, },
async completeOrder() { async completeOrder(autoPrint = true) {
if (this.items.length === 0) { if (this.items.length === 0) {
Swal.fire(<?= json_encode($lang === 'ar' ? 'خطأ' : 'Error', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, <?= json_encode($lang === 'ar' ? 'السلة فارغة' : 'Cart is empty', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, 'error'); Swal.fire(<?= json_encode($lang === 'ar' ? 'خطأ' : 'Error', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, <?= json_encode($lang === 'ar' ? 'السلة فارغة' : 'Cart is empty', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, 'error');
return; return;
@ -8421,10 +8426,34 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
return; return;
} }
const btn = document.getElementById('confirmPaymentBtn'); const activeBtn = document.getElementById(autoPrint ? 'confirmPaymentPrintBtn' : 'confirmPaymentSaveBtn');
const originalText = btn.innerText; const actionButtons = Array.from(document.querySelectorAll('.pos-complete-order-btn'));
btn.disabled = true; const originalButtonHtml = actionButtons.reduce((carry, button) => {
btn.innerText = <?= json_encode($lang === 'ar' ? 'جارٍ المعالجة...' : 'PROCESSING...', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>; carry[button.id] = button.innerHTML;
return carry;
}, {});
actionButtons.forEach(button => {
button.disabled = true;
});
if (activeBtn) {
activeBtn.innerText = <?= json_encode($lang === 'ar' ? 'جارٍ المعالجة...' : 'PROCESSING...', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
}
const restoreActionButtons = () => {
actionButtons.forEach(button => {
if (Object.prototype.hasOwnProperty.call(originalButtonHtml, button.id)) {
button.innerHTML = originalButtonHtml[button.id];
}
button.disabled = false;
});
};
const savedTitle = <?= json_encode($lang === 'ar' ? 'تم الحفظ' : 'Saved', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const savedWithoutPrintText = <?= json_encode($lang === 'ar' ? 'تم حفظ الفاتورة بدون طباعة' : 'Invoice saved without printing', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const savedWithoutPrintPrefix = <?= json_encode($lang === 'ar' ? 'تم حفظ الفاتورة بدون طباعة. الرقم:' : 'Invoice saved without printing. No:', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const okText = <?= json_encode($lang === 'ar' ? 'حسنًا' : 'OK', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const subtotal = this.items.reduce((sum, item) => sum + (parseFloat(item.price) * item.qty), 0); const subtotal = this.items.reduce((sum, item) => sum + (parseFloat(item.price) * item.qty), 0);
const totalVat = this.items.reduce((sum, item) => { const totalVat = this.items.reduce((sum, item) => {
@ -8465,22 +8494,34 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
} }
if (result.success) { if (result.success) {
const payModal = bootstrap.Modal.getInstance(document.getElementById('posPaymentModal')); const payModalEl = document.getElementById('posPaymentModal');
const payModal = payModalEl ? bootstrap.Modal.getInstance(payModalEl) : null;
if (payModal) payModal.hide(); if (payModal) payModal.hide();
this.showReceipt(result.invoice_id, discountAmount, loyaltyRedeemed, result.transaction_no);
if (autoPrint) {
this.showReceipt(result.invoice_id, discountAmount, loyaltyRedeemed, result.transaction_no, true);
} else {
this.clear();
Swal.fire({
icon: 'success',
title: savedTitle,
text: result.transaction_no ? `${savedWithoutPrintPrefix} ${result.transaction_no}` : savedWithoutPrintText,
confirmButtonText: okText
}).then(() => {
location.reload();
});
}
} else { } else {
Swal.fire(<?= json_encode($lang === 'ar' ? 'خطأ' : 'Error', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, result.error, 'error'); Swal.fire(<?= json_encode($lang === 'ar' ? 'خطأ' : 'Error', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, result.error, 'error');
btn.disabled = false; restoreActionButtons();
btn.innerText = originalText;
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
Swal.fire(<?= json_encode($lang === 'ar' ? 'خطأ' : 'Error', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, err.message || <?= json_encode($lang === 'ar' ? 'حدث خطأ ما' : 'Something went wrong', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, 'error'); Swal.fire(<?= json_encode($lang === 'ar' ? 'خطأ' : 'Error', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, err.message || <?= json_encode($lang === 'ar' ? 'حدث خطأ ما' : 'Something went wrong', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, 'error');
btn.disabled = false; restoreActionButtons();
btn.innerText = originalText;
} }
}, },
showReceipt(invId, discountAmount, loyaltyRedeemed, transactionNo) { showReceipt(invId, discountAmount, loyaltyRedeemed, transactionNo, autoPrint = false) {
const container = document.getElementById('posReceiptContent'); const container = document.getElementById('posReceiptContent');
const customerSelect = document.getElementById('posCustomer'); const customerSelect = document.getElementById('posCustomer');
const customerName = (customerSelect && customerSelect.selectedIndex >= 0 && customerSelect.value !== '') ? customerSelect.options[customerSelect.selectedIndex].text : <?= json_encode(__('walk_in_customer'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>; const customerName = (customerSelect && customerSelect.selectedIndex >= 0 && customerSelect.value !== '') ? customerSelect.options[customerSelect.selectedIndex].text : <?= json_encode(__('walk_in_customer'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
@ -8589,14 +8630,23 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
</div> </div>
`; `;
const modal = new bootstrap.Modal(document.getElementById('posReceiptModal')); const receiptModalEl = document.getElementById('posReceiptModal');
const modal = bootstrap.Modal.getOrCreateInstance(receiptModalEl);
modal.show(); modal.show();
this.clear(); this.clear();
document.getElementById('posReceiptModal').addEventListener('hidden.bs.modal', function () { receiptModalEl.addEventListener('hidden.bs.modal', function () {
location.reload(); location.reload();
}, { once: true }); }, { once: true });
if (autoPrint) {
setTimeout(() => {
if (typeof window.printPosReceipt === 'function') {
window.printPosReceipt();
}
}, 250);
}
}, },
async syncCustomer(val) { async syncCustomer(val) {
@ -12198,7 +12248,9 @@ document.addEventListener('DOMContentLoaded', function() {
#posPaymentModal .form-control { font-size: 0.85rem; padding: 0.35rem 0.6rem; } #posPaymentModal .form-control { font-size: 0.85rem; padding: 0.35rem 0.6rem; }
#posPaymentModal .btn-primary { padding: 0.35rem 0.8rem; font-size: 0.85rem; } #posPaymentModal .btn-primary { padding: 0.35rem 0.8rem; font-size: 0.85rem; }
#posPaymentModal .modal-header { padding: 0.75rem 1rem 0.25rem; } #posPaymentModal .modal-header { padding: 0.75rem 1rem 0.25rem; }
#posPaymentModal .modal-footer { padding: 0.25rem 1rem 0.75rem; } #posPaymentModal .modal-footer { padding: 0.25rem 1rem 0.75rem; gap: 0.5rem; flex-wrap: wrap; }
#posPaymentModal .modal-footer .btn-light { min-width: 110px; }
#posPaymentModal .modal-footer .pos-complete-order-btn { flex: 1 1 180px; }
</style> </style>
<!-- POS Payment Modal --> <!-- POS Payment Modal -->
@ -12299,8 +12351,11 @@ document.addEventListener('DOMContentLoaded', function() {
</div> </div>
<div class="modal-footer border-0"> <div class="modal-footer border-0">
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button> <button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
<button type="button" class="btn btn-primary px-4" id="confirmPaymentBtn" onclick="cart.completeOrder()"> <button type="button" class="btn btn-outline-primary pos-complete-order-btn" id="confirmPaymentSaveBtn" onclick="cart.completeOrder(false)" data-en="Save Without Print" data-ar="حفظ بدون طباعة">
<?= $lang === 'ar' ? 'ادفع وأكمل' : 'PAY & COMPLETE' ?> <?= $lang === 'ar' ? 'حفظ بدون طباعة' : 'SAVE WITHOUT PRINT' ?>
</button>
<button type="button" class="btn btn-primary pos-complete-order-btn" id="confirmPaymentPrintBtn" onclick="cart.completeOrder(true)" data-en="Save & Print" data-ar="حفظ وطباعة">
<?= $lang === 'ar' ? 'حفظ وطباعة' : 'SAVE & PRINT' ?>
</button> </button>
</div> </div>
</div> </div>

View File

@ -852,14 +852,158 @@
printArea.innerHTML = `<div class="thermal-receipt thermal-receipt-print">${sourceHtml || ''}</div>`; printArea.innerHTML = `<div class="thermal-receipt thermal-receipt-print">${sourceHtml || ''}</div>`;
}; };
function printPosReceipt() { window.printReceiptHtml = function(sourceHtml, options = {}) {
const content = document.getElementById('posReceiptContent').innerHTML; const rawHtml = (sourceHtml || '').trim();
if (!rawHtml) {
return;
}
const wrapper = document.createElement('div');
wrapper.innerHTML = rawHtml;
const receipt = wrapper.querySelector('.thermal-receipt');
const receiptHtml = receipt ? receipt.outerHTML : `<div class="thermal-receipt">${rawHtml}</div>`;
const docTitle = String(options.title || 'Receipt').replace(/[<>"']/g, '');
const pageLang = document.documentElement.getAttribute('lang') || 'en';
const pageDir = document.documentElement.getAttribute('dir') || (receipt && receipt.classList.contains('rtl') ? 'rtl' : 'ltr');
const iframe = document.createElement('iframe');
iframe.setAttribute('aria-hidden', 'true');
iframe.style.position = 'fixed';
iframe.style.right = '0';
iframe.style.bottom = '0';
iframe.style.width = '0';
iframe.style.height = '0';
iframe.style.border = '0';
document.body.appendChild(iframe);
const frameDoc = iframe.contentWindow.document;
frameDoc.open();
frameDoc.write(`<!doctype html>
<html lang="${pageLang}" dir="${pageDir}">
<head>
<meta charset="utf-8">
<title>${docTitle}</title>
<style>
* { box-sizing: border-box; }
@page { size: 80mm auto; margin: 0; }
html, body {
width: 80mm !important;
max-width: 80mm !important;
margin: 0 auto !important;
padding: 0 !important;
background: #fff !important;
color: #000 !important;
}
body {
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
line-height: 1.4;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
img {
max-width: 100%;
height: auto;
}
h5, h6, p {
margin: 0 0 4px 0;
}
.thermal-receipt {
width: 72mm !important;
max-width: 72mm !important;
margin: 0 auto !important;
padding: 2mm 0 !important;
background: #fff !important;
color: #000 !important;
}
.thermal-receipt.rtl {
direction: rtl;
text-align: right;
}
.thermal-receipt .center {
text-align: center;
}
.thermal-receipt .separator {
border-top: 1px dashed #000;
margin: 8px 0;
}
.thermal-receipt table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.thermal-receipt table th {
text-align: left;
border-bottom: 1px dashed #000;
font-size: 10px;
padding-bottom: 4px;
word-break: break-word;
}
.thermal-receipt.rtl table th {
text-align: right;
}
.thermal-receipt table td {
padding: 4px 0;
font-size: 11px;
word-break: break-word;
vertical-align: top;
}
.d-flex { display: flex !important; }
.justify-content-between { justify-content: space-between !important; }
.align-items-center { align-items: center !important; }
.fw-bold { font-weight: 700 !important; }
.small { font-size: 11px !important; }
.text-uppercase { text-transform: uppercase !important; }
.text-danger { color: #b91c1c !important; }
.text-success { color: #047857 !important; }
.mb-0 { margin-bottom: 0 !important; }
.mt-1 { margin-top: 0.25rem !important; }
.total-row {
font-weight: 700;
font-size: 13px;
}
</style>
</head>
<body>${receiptHtml}</body>
</html>`);
frameDoc.close();
setTimeout(() => {
iframe.contentWindow.focus();
iframe.contentWindow.print();
setTimeout(() => {
document.body.removeChild(iframe);
}, 2000);
}, 350);
};
window.printPosReceipt = function(options = {}) {
const contentEl = document.getElementById('posReceiptContent');
if (!contentEl) {
return;
}
const content = contentEl.innerHTML;
if (!(content || '').trim()) {
return;
}
if (typeof window.printReceiptHtml === 'function') {
window.printReceiptHtml(content, {
title: options.title || <?= json_encode($lang === 'ar' ? 'إيصال نقطة البيع' : 'POS Receipt', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>
});
return;
}
const printArea = document.getElementById('posPrintArea'); const printArea = document.getElementById('posPrintArea');
window.prepareReceiptPrintArea(content, printArea); window.prepareReceiptPrintArea(content, printArea);
document.body.classList.add('printing-receipt'); document.body.classList.add('printing-receipt');
window.print(); window.print();
document.body.classList.remove('printing-receipt'); document.body.classList.remove('printing-receipt');
location.reload(); if (printArea) {
printArea.innerHTML = '';
}
} }

View File

@ -200,27 +200,34 @@
setTimeout(() => { setTimeout(() => {
const receiptContent = document.getElementById('posReceiptContent'); const receiptContent = document.getElementById('posReceiptContent');
const printArea = document.getElementById('posPrintArea'); if (!receiptContent) {
if (!receiptContent || !printArea) {
return; return;
} }
if (typeof window.prepareReceiptPrintArea === 'function') { if (typeof window.printReceiptHtml === 'function') {
window.prepareReceiptPrintArea(receiptContent.innerHTML, printArea); window.printReceiptHtml(receiptContent.innerHTML, {
title: data.document_no || data.transaction_no || 'Sale Receipt'
});
} else { } else {
printArea.innerHTML = receiptContent.innerHTML; const printArea = document.getElementById('posPrintArea');
const receipt = printArea.querySelector('.thermal-receipt'); if (printArea) {
if (receipt) { if (typeof window.prepareReceiptPrintArea === 'function') {
receipt.classList.add('thermal-receipt-print'); window.prepareReceiptPrintArea(receiptContent.innerHTML, printArea);
} else {
printArea.innerHTML = receiptContent.innerHTML;
const receipt = printArea.querySelector('.thermal-receipt');
if (receipt) {
receipt.classList.add('thermal-receipt-print');
}
}
document.body.classList.add('printing-receipt');
window.print();
document.body.classList.remove('printing-receipt');
printArea.innerHTML = '';
} }
} }
document.body.classList.add('printing-receipt');
window.print();
document.body.classList.remove('printing-receipt');
printArea.innerHTML = '';
const receiptModalEl = document.getElementById('posReceiptModal'); const receiptModalEl = document.getElementById('posReceiptModal');
const receiptModal = receiptModalEl ? bootstrap.Modal.getInstance(receiptModalEl) : null; const receiptModal = receiptModalEl ? bootstrap.Modal.getInstance(receiptModalEl) : null;
if (receiptModal) { if (receiptModal) {