38471-vm/pages/barcode_pos_script.php
2026-05-06 10:49:22 +00:00

378 lines
15 KiB
PHP

function escapeBarcodeLabelHtml(value) {
return String(value || '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function getSingleBarcodeLabelData() {
return window.currentSingleBarcodeLabel || null;
}
function getSingleBarcodePrintDimensions() {
return {
width: Math.max(parseInt(document.getElementById('barcodeWidth').value, 10) || 50, 10),
height: Math.max(parseInt(document.getElementById('barcodeHeight').value, 10) || 30, 10)
};
}
function getSingleBarcodeOptions(sku, labelWidthMm, labelHeightMm) {
const skuText = String(sku || '');
const skuLength = skuText.length;
const compact = labelWidthMm <= 40 || labelHeightMm <= 25;
const extraCompact = labelWidthMm <= 32 || labelHeightMm <= 20;
const showValue = !extraCompact && labelHeightMm >= 24 && skuLength <= 18;
let moduleWidth = 1.8;
if (skuLength >= 18 || labelWidthMm <= 35) {
moduleWidth = 1.1;
} else if (skuLength >= 14 || labelWidthMm <= 40) {
moduleWidth = 1.3;
} else if (labelWidthMm <= 50) {
moduleWidth = 1.6;
}
let barcodeHeight = 56;
if (labelHeightMm <= 20) {
barcodeHeight = 30;
} else if (labelHeightMm <= 25) {
barcodeHeight = 38;
} else if (labelHeightMm <= 30) {
barcodeHeight = 46;
} else if (labelHeightMm <= 35) {
barcodeHeight = 56;
} else {
barcodeHeight = 64;
}
const quietZone = extraCompact ? 4 : compact ? 6 : 10;
return {
format: "CODE128",
lineColor: "#000",
width: moduleWidth,
height: barcodeHeight,
displayValue: showValue,
fontSize: compact ? 10 : 12,
textMargin: compact ? 2 : 4,
margin: quietZone,
marginTop: 0,
marginBottom: showValue ? 2 : 0,
fontOptions: "bold"
};
}
function buildSingleBarcodeNameHtml(nameAr, nameEn) {
const lines = [];
if (nameAr) {
lines.push('<div class="label-name-ar">' + escapeBarcodeLabelHtml(nameAr) + '</div>');
}
if (nameEn) {
lines.push('<div class="label-name-en">' + escapeBarcodeLabelHtml(nameEn) + '</div>');
}
return lines.join('');
}
function buildSingleBarcodeSvgMarkup(sku, labelWidthMm, labelHeightMm) {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
JsBarcode(svg, sku, getSingleBarcodeOptions(sku, labelWidthMm, labelHeightMm));
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
svg.style.width = '100%';
svg.style.height = 'auto';
return svg.outerHTML;
}
function renderSingleBarcodePreview() {
const label = getSingleBarcodeLabelData();
if (!label) {
return;
}
const dimensions = getSingleBarcodePrintDimensions();
const width = dimensions.width;
const height = dimensions.height;
const nameContainer = document.getElementById('barcodeLabelName');
const priceContainer = document.getElementById('barcodeLabelPrice');
const previewContainer = document.getElementById('barcodeContainer');
const svg = document.getElementById('barcodeSvg');
if (!nameContainer || !priceContainer || !previewContainer || !svg) {
return;
}
nameContainer.innerHTML = buildSingleBarcodeNameHtml(label.nameAr, label.nameEn);
priceContainer.textContent = label.price ? 'OMR ' + label.price : '';
previewContainer.style.width = Math.min(Math.max(width * 4, 180), 320) + 'px';
previewContainer.style.maxWidth = '100%';
previewContainer.style.padding = height <= 25 ? '12px' : '16px';
svg.innerHTML = '';
JsBarcode(svg, label.sku, getSingleBarcodeOptions(label.sku, width, height));
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
svg.style.width = '100%';
svg.style.height = 'auto';
}
function initSingleBarcodePreviewControls() {
['barcodeWidth', 'barcodeHeight'].forEach((id) => {
const input = document.getElementById(id);
if (!input || input.dataset.barcodePreviewBound === '1') {
return;
}
input.dataset.barcodePreviewBound = '1';
input.addEventListener('input', renderSingleBarcodePreview);
input.addEventListener('change', renderSingleBarcodePreview);
});
}
initSingleBarcodePreviewControls();
window.printItemBarcode = function(sku, nameAr, nameEn, price) {
if (!sku) {
Swal.fire('Error', 'This item has no SKU/Barcode assigned.', 'error');
return;
}
window.currentSingleBarcodeLabel = {
sku: String(sku),
nameAr: nameAr || '',
nameEn: nameEn || '',
price: price || ''
};
renderSingleBarcodePreview();
const modal = new bootstrap.Modal(document.getElementById('barcodePrintModal'));
modal.show();
};
window.executeBarcodePrint = function() {
const qty = Math.max(parseInt(document.getElementById('barcodeQty').value, 10) || 1, 1);
const dimensions = getSingleBarcodePrintDimensions();
const width = dimensions.width;
const height = dimensions.height;
const label = getSingleBarcodeLabelData();
if (!label) {
return;
}
const nameHtml = buildSingleBarcodeNameHtml(label.nameAr, label.nameEn);
const price = label.price ? 'OMR ' + escapeBarcodeLabelHtml(label.price) : '';
const svg = buildSingleBarcodeSvgMarkup(label.sku, width, height);
const compactClass = width <= 40 || height <= 25 ? 'label-compact' : '';
const iframe = document.createElement('iframe');
iframe.style.position = 'absolute';
iframe.style.width = '0px';
iframe.style.height = '0px';
iframe.style.border = 'none';
document.body.appendChild(iframe);
const doc = iframe.contentWindow.document;
let labelsHtml = '';
for (let i = 0; i < qty; i++) {
labelsHtml += `
<div class="label-container ${compactClass}">
${nameHtml ? `<div class="label-name">${nameHtml}</div>` : ''}
<div class="barcode-wrap">${svg}</div>
${price ? `<div class="label-price">${price}</div>` : ''}
</div>
`;
}
doc.open();
doc.write(`
<html>
<head>
<style>
@page { size: ${width}mm ${height}mm; margin: 0; }
body { margin: 0; padding: 0; font-family: sans-serif; }
.label-container {
width: ${width}mm;
height: ${height}mm;
page-break-after: always;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
text-align: center;
overflow: hidden;
box-sizing: border-box;
padding: 1.5mm 2mm;
gap: 0.75mm;
}
.label-container:last-child { page-break-after: avoid; }
.label-name {
width: 100%;
max-width: 100%;
line-height: 1.05;
}
.label-name-ar,
.label-name-en {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.label-name-ar {
font-weight: 700;
font-size: 11px;
direction: rtl;
}
.label-name-en { font-size: 9px; }
.label-price {
font-size: 11px;
font-weight: 700;
line-height: 1;
white-space: nowrap;
}
.label-compact {
padding: 1mm 1.5mm;
gap: 0.5mm;
}
.label-compact .label-name-ar { font-size: 10px; }
.label-compact .label-name-en { font-size: 8px; }
.label-compact .label-price { font-size: 10px; }
.barcode-wrap {
width: 100%;
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 0 1.5mm;
overflow: hidden;
}
.barcode-wrap svg {
width: 100%;
height: auto;
display: block;
overflow: visible;
}
</style>
</head>
<body>
${labelsHtml}
</body>
</html>
`);
doc.close();
iframe.contentWindow.focus();
setTimeout(() => {
iframe.contentWindow.print();
setTimeout(() => {
document.body.removeChild(iframe);
}, 2000);
}, 500);
};
<?php require 'pages/sales_purchases_print_script.php'; ?>
window.printPosReceiptFromInvoice = function(inv) {
const container = document.getElementById('posReceiptContent');
const itemsHtml = inv.items.map(item => {
const itemTotal = item.unit_price * item.quantity;
const vatRate = parseFloat(item.vat_rate !== undefined && item.vat_rate !== null ? item.vat_rate : 0);
const vatAmount = itemTotal * (vatRate / (100 + vatRate));
return `
<tr>
<td>${item.name_en} / ${item.name_ar}<br><small>${item.quantity} x ${parseFloat(item.unit_price).toFixed(3)}</small></td>
<td style="text-align: right; vertical-align: bottom;">${vatAmount.toFixed(2)}</td>
<td style="text-align: right; vertical-align: bottom;">${itemTotal.toFixed(3)}</td>
</tr>
`;
}).join('');
const totalVat = inv.items.reduce((sum, item) => {
const itemTotal = item.unit_price * item.quantity;
const vatRate = parseFloat(item.vat_rate !== undefined && item.vat_rate !== null ? item.vat_rate : 0);
return sum + (itemTotal * (vatRate / (100 + vatRate)));
}, 0);
const subtotal = inv.items.reduce((sum, item) => sum + (item.unit_price * item.quantity), 0);
const companyName = "<?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>";
const outletName = "<?= htmlspecialchars($data['settings']['current_outlet_name'] ?? '') ?>";
const companyPhone = "<?= htmlspecialchars($data['settings']['company_phone'] ?? '') ?>";
const companyVat = "<?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?>";
const companyLogo = "<?= htmlspecialchars($data['settings']['company_logo'] ?? '') ?>";
container.innerHTML = `
<div class="thermal-receipt">
<div class="center">
${companyLogo ? `<img src="${companyLogo}" alt="Logo" style="max-height: 60px; width: auto; margin-bottom: 10px; display: block; margin-left: auto; margin-right: auto;">` : ''}
<h5 class="mb-0 fw-bold">${companyName}</h5>
${inv.outlet_name ? `<div class="fw-bold text-uppercase">${inv.outlet_name}</div>` : ''}
${companyPhone ? `<div>Tel: ${companyPhone}</div>` : ''}
${companyVat ? `<div>VAT: ${companyVat}</div>` : ''}
<div class="separator"></div>
<h6 class="fw-bold">TAX INVOICE / فاتورة ضريبية</h6>
<div>Inv / رقم: INV-${inv.id.toString().padStart(5, '0')}</div>
<div>Date / التاريخ: ${inv.invoice_date}</div>
<div class="separator"></div>
</div>
<div>
<strong>Customer / العميل:</strong> ${inv.customer_name || 'Walk-in / عميل عابر'}
</div>
<div class="separator"></div>
<table>
<thead>
<tr>
<th>ITEM / الصنف</th>
<th style="text-align: right;">VAT / الضريبة</th>
<th style="text-align: right;">TOTAL / الإجمالي</th>
</tr>
</thead>
<tbody>
${itemsHtml}
</tbody>
</table>
<div class="separator"></div>
<div class="d-flex justify-content-between small">
<span>Subtotal (Excl. VAT) / المجموع الفرعي (دون الضريبة)</span>
<span><?= __('currency') ?> ${(subtotal - totalVat).toFixed(3)}</span>
</div>
<div class="d-flex justify-content-between small">
<span>VAT / الضريبة</span>
<span><?= __('currency') ?> ${totalVat.toFixed(2)}</span>
</div>
<div class="total-row d-flex justify-content-between">
<span>TOTAL (Incl. VAT) / الإجمالي (شامل الضريبة)</span>
<span><?= __('currency') ?> ${subtotal.toFixed(3)}</span>
</div>
<div class="d-flex justify-content-between small">
<span>PAID / المدفوع</span>
<span><?= __('currency') ?> ${parseFloat(inv.paid_amount).toFixed(3)}</span>
</div>
<div class="d-flex justify-content-between small fw-bold">
<span>BALANCE / الرصيد</span>
<span><?= __('currency') ?> ${(subtotal - inv.paid_amount).toFixed(3)}</span>
</div>
<div class="separator"></div>
<div class="center small">
<p>Thank You for your business! / شكراً لتعاملكم معنا!</p>
</div>
</div>
`;
const posModal = new bootstrap.Modal(document.getElementById('posReceiptModal'));
posModal.show();
};
function printPosReceipt() {
const content = document.getElementById('posReceiptContent').innerHTML;
const printArea = document.getElementById('posPrintArea');
printArea.innerHTML = `<div class="thermal-receipt thermal-receipt-print">${content}</div>`;
document.body.classList.add('printing-receipt');
window.print();
document.body.classList.remove('printing-receipt');
location.reload();
}