new updates

This commit is contained in:
Flatlogic Bot 2026-05-04 05:24:49 +00:00
parent 8df558e09d
commit ba54d3065c
5 changed files with 382 additions and 69 deletions

View File

@ -367,7 +367,7 @@ require_once 'includes/header.php';
<span class="badge <?= h(payment_status_badge_class($paymentSummary['payment_status'])) ?>"><?= h(payment_status_label($paymentSummary['payment_status'])) ?></span>
</td>
<td>
<button class="btn btn-sm btn-outline-success rounded-pill px-3" onclick="receivePayment(<?= (int) $sale['id'] ?>, <?= json_encode((float) $paymentSummary['due_amount']) ?>, <?= ($sale['status'] ?? 'completed') === 'order' ? 'true' : 'false' ?>)">
<button class="btn btn-sm btn-outline-success rounded-pill px-3" onclick="receivePayment(<?= (int) $sale['id'] ?>, <?= json_encode((float) $sale['total_amount']) ?>, <?= json_encode((float) $paymentSummary['paid_amount']) ?>, <?= json_encode((float) $paymentSummary['due_amount']) ?>, <?= ($sale['status'] ?? 'completed') === 'order' ? 'true' : 'false' ?>)">
<i class="bi bi-cash-coin"></i> <?= h(tr('استلام دفعة', 'Receive Payment')) ?>
</button>
</td>
@ -383,30 +383,80 @@ require_once 'includes/header.php';
</div>
<script>
async function receivePayment(id, dueAmount, completeOrder = false) {
const { value: paymentAmount } = await Swal.fire({
function formatPaymentPopupAmount(value) {
return Number(value || 0).toFixed(3);
}
async function receivePayment(id, totalAmount, paidAmount, dueAmount, completeOrder = false) {
const popupHtml = `
<div class="text-start">
<div class="row g-2 mb-3">
<div class="col-4">
<div class="border rounded-3 p-2 h-100 bg-light">
<div class="small text-muted"><?= h(tr('إجمالي الفاتورة', 'Total amount')) ?></div>
<div class="fw-bold text-dark">${formatPaymentPopupAmount(totalAmount)}</div>
</div>
</div>
<div class="col-4">
<div class="border rounded-3 p-2 h-100 bg-light">
<div class="small text-muted"><?= h(tr('المدفوع سابقاً', 'Already paid')) ?></div>
<div class="fw-bold text-primary">${formatPaymentPopupAmount(paidAmount)}</div>
</div>
</div>
<div class="col-4">
<div class="border rounded-3 p-2 h-100 bg-light">
<div class="small text-muted"><?= h(tr('المتبقي الحالي', 'Current remaining')) ?></div>
<div class="fw-bold text-danger">${formatPaymentPopupAmount(dueAmount)}</div>
</div>
</div>
</div>
<label for="swal-payment-amount" class="form-label fw-semibold mb-2"><?= h(tr('المبلغ المطلوب دفعه الآن', 'Amount to pay now')) ?></label>
<input id="swal-payment-amount" type="number" class="swal2-input mt-0" min="0.001" step="0.001" max="${formatPaymentPopupAmount(dueAmount)}" value="${formatPaymentPopupAmount(dueAmount)}">
<div class="d-flex justify-content-between align-items-center rounded-3 border px-3 py-2 bg-light mt-3">
<span class="small text-muted"><?= h(tr('المتبقي بعد الدفعة', 'Remaining after payment')) ?></span>
<strong id="swal-payment-remaining" class="text-success">0.000</strong>
</div>
</div>`;
const { isConfirmed, value: paymentAmount } = await Swal.fire({
title: '<?= h(tr('استلام دفعة', 'Receive Payment')) ?>',
text: '<?= h(tr('أدخل المبلغ المستلم لهذه الفاتورة.', 'Enter the amount received for this invoice.')) ?>',
input: 'number',
inputAttributes: { min: '0.001', step: '0.001', max: String(dueAmount) },
inputValue: Number(dueAmount).toFixed(3),
html: popupHtml,
showCancelButton: true,
confirmButtonColor: '#198754',
confirmButtonText: '<?= h(tr('حفظ الدفعة', 'Save Payment')) ?>',
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>',
inputValidator: (value) => {
const amount = parseFloat(value || '0');
focusConfirm: false,
didOpen: () => {
const input = document.getElementById('swal-payment-amount');
const remainingEl = document.getElementById('swal-payment-remaining');
const updateRemaining = () => {
const amount = parseFloat(input.value || '0');
const safeAmount = Number.isFinite(amount) ? amount : 0;
const remaining = Math.max(dueAmount - safeAmount, 0);
remainingEl.textContent = formatPaymentPopupAmount(remaining);
remainingEl.className = remaining > 0.0005 ? 'text-danger' : 'text-success';
};
input.addEventListener('input', updateRemaining);
input.focus();
input.select();
updateRemaining();
},
preConfirm: () => {
const input = document.getElementById('swal-payment-amount');
const amount = parseFloat(input.value || '0');
if (!amount || amount <= 0) {
return '<?= h(tr('أدخل مبلغاً صحيحاً.', 'Enter a valid amount.')) ?>';
Swal.showValidationMessage('<?= h(tr('أدخل مبلغاً صحيحاً.', 'Enter a valid amount.')) ?>');
return false;
}
if (amount - dueAmount > 0.0005) {
return '<?= h(tr('المبلغ لا يمكن أن يتجاوز المتبقي.', 'Amount cannot exceed the due balance.')) ?>';
Swal.showValidationMessage('<?= h(tr('المبلغ لا يمكن أن يتجاوز المتبقي.', 'Amount cannot exceed the due balance.')) ?>');
return false;
}
return null;
return formatPaymentPopupAmount(amount);
}
});
if (!paymentAmount) {
if (!isConfirmed || !paymentAmount) {
return;
}

View File

@ -32,6 +32,12 @@ $deliveryStatusInput = trim((string) ($_POST['delivery_status'] ?? ($editSale['d
$deliveryDateInput = trim((string) ($_POST['delivery_date'] ?? ($editSale['delivery_date'] ?? '')));
$notesInput = trim((string) ($_POST['notes'] ?? ($editSale['notes'] ?? '')));
$saleStatusInput = trim((string) ($_POST['sale_status'] ?? ($editSale['status'] ?? 'completed')));
$itemNotePlaceholder = $isEidSale
? tr('مثال: بدون سكر، تغليف هدية، لون معين، كتابة اسم...', 'Example: no sugar, gift wrap, specific color, write a name...')
: tr('ملاحظة داخلية لهذا الصنف...', 'Internal note for this item...');
$itemNoteHelper = $isEidSale
? tr('ملاحظة داخلية لهذا الصنف فقط — لا تُطبع في الإيصال.', 'Internal for this item only — not printed on the receipt.')
: tr('ملاحظة محفوظة لهذا الصنف داخل النظام.', 'This note is saved for this item inside the system.');
try {
$customers = db()->query('SELECT id, name, phone FROM customers ORDER BY name ASC')->fetchAll();
@ -79,6 +85,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
continue;
}
$product = $catalog[$sku];
$itemNote = trim((string) ($item['item_note'] ?? ''));
$price = (float) $product['price'];
$lineTotal = $price * $qty;
@ -92,6 +99,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'name_en' => $product['name_en'],
'qty' => $qty,
'price' => $price,
'item_note' => $itemNote,
'line_total' => $lineTotal,
'vat_percent' => $vatPercent,
'vat_amount' => $itemVat
@ -560,6 +568,9 @@ require __DIR__ . '/includes/header.php';
const catalogData = <?= json_encode($catalog, JSON_UNESCAPED_UNICODE) ?>;
const catalogArray = Object.values(catalogData);
const partialPaymentText = '<?= h(tr('متبقٍ بعد الحفظ:', 'Due after save:')) ?>';
const showItemNotes = <?= $isEidSale ? 'true' : 'false' ?>;
const itemNotePlaceholder = <?= json_encode($itemNotePlaceholder, JSON_UNESCAPED_UNICODE) ?>;
const itemNoteHelper = <?= json_encode($itemNoteHelper, JSON_UNESCAPED_UNICODE) ?>;
let invoiceItems = {};
let currentInvoiceTotal = 0;
@ -570,7 +581,8 @@ initialItemsJson.forEach(item => {
sku: item.sku,
name: '<?= current_lang() ?>' === 'ar' ? item.name_ar : item.name_en,
price: parseFloat(item.price),
qty: parseInt(item.qty)
qty: parseInt(item.qty),
item_note: item.item_note || ''
};
});
// renderInvoice();
@ -746,7 +758,8 @@ function addItemToInvoice(sku) {
sku: sku,
name: '<?= current_lang() ?>' === 'ar' ? item.name_ar : item.name_en,
price: parseFloat(item.price),
qty: 1
qty: 1,
item_note: ''
};
}
renderInvoice();
@ -767,6 +780,27 @@ function removeItem(sku) {
renderInvoice();
}
function changeItemNoteValue(sku, newNote) {
if (!invoiceItems[sku]) {
return;
}
invoiceItems[sku].item_note = String(newNote ?? '');
cartJson.value = JSON.stringify(Object.keys(invoiceItems).map(currentSku => {
const currentItem = invoiceItems[currentSku];
return {
sku: currentItem.sku,
qty: currentItem.qty,
item_note: String(currentItem.item_note ?? '')
};
}));
}
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, function(char) {
return ({'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'})[char] || char;
});
}
function renderInvoice() {
const skus = Object.keys(invoiceItems);
if (skus.length === 0) {
@ -791,13 +825,22 @@ function renderInvoice() {
totalVat += itemVat;
totalAmount += lineTotal;
cartData.push({ sku: item.sku, qty: item.qty });
cartData.push({ sku: item.sku, qty: item.qty, item_note: String(item.item_note || '') });
const safeName = escapeHtml(item.name);
const safeSku = escapeHtml(item.sku);
const safeItemNote = escapeHtml(item.item_note || '');
const itemNoteHtml = showItemNotes ? `
<input type="text" class="form-control form-control-sm line-input mt-2" value="${safeItemNote}" oninput="changeItemNoteValue('${sku}', this.value)" placeholder="${escapeHtml(itemNotePlaceholder)}">
<div class="text-muted small mt-1">${escapeHtml(itemNoteHelper)}</div>
` : '';
const tr = document.createElement('tr');
tr.innerHTML = `
<td>
<div class="fw-medium text-dark">${item.name}</div>
<div class="text-muted small">SKU: ${item.sku}</div>
<div class="fw-medium text-dark">${safeName}</div>
${itemNoteHtml}
<div class="text-muted small mt-2">SKU: ${safeSku}</div>
</td>
<td class="text-center text-muted align-middle">${item.price.toFixed(3)}</td>
<td class="text-center align-middle">

View File

@ -34,6 +34,26 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
redirect_to('eid_orders.php', $_GET);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'delete_order') {
$id = (int) ($_POST['id'] ?? 0);
try {
$stmt = db()->prepare("DELETE FROM sales_orders WHERE id = :id AND order_type = :order_type LIMIT 1");
$stmt->execute([
':id' => $id,
':order_type' => 'eid',
]);
if ($stmt->rowCount() > 0) {
set_flash('success', tr('تم حذف طلب العيد بنجاح.', 'Eid order deleted successfully.'));
} else {
set_flash('warning', tr('تعذر العثور على طلب العيد المطلوب حذفه.', 'The Eid order to delete could not be found.'));
}
} catch (Throwable $e) {
set_flash('danger', tr('تعذر حذف طلب العيد.', 'Failed to delete the Eid order.'));
}
redirect_to('eid_orders.php', $_GET);
}
$activeNav = 'eid_orders';
$pageTitle = tr('طلبات العيد', 'Eid Orders');
@ -481,13 +501,9 @@ require __DIR__ . '/includes/header.php';
<td>
<div class="d-flex justify-content-end gap-2">
<?php if ((($order['payment_summary']['payment_status'] ?? ($order['payment_status'] ?? 'unpaid'))) !== 'paid'): ?>
<form method="post" action="" class="d-inline js-confirm-mark-paid" data-confirm-message="<?= h(tr('هل أنت متأكد من تغيير حالة الدفع إلى مدفوع؟', 'Are you sure you want to mark as paid?')) ?>" data-confirm-title="<?= h(tr('تأكيد الإجراء', 'Confirm action')) ?>">
<input type="hidden" name="action" value="mark_as_paid">
<input type="hidden" name="id" value="<?= h($order['id']) ?>">
<button type="submit" class="btn btn-sm btn-outline-success" title="<?= h(tr('تحويل لمدفوع', 'Mark as Paid')) ?>">
<i class="bi bi-cash"></i>
</button>
</form>
<button type="button" class="btn btn-sm btn-outline-success" onclick="receivePayment(<?= (int) $order['id'] ?>, <?= json_encode((float) ($order['total_amount'] ?? 0)) ?>, <?= json_encode((float) (($order['payment_summary']['paid_amount'] ?? 0))) ?>, <?= json_encode((float) (($order['payment_summary']['due_amount'] ?? 0))) ?>, <?= (($order['status'] ?? 'completed') === 'order') ? 'true' : 'false' ?>)" title="<?= h(tr('استلام دفعة', 'Receive Payment')) ?>">
<i class="bi bi-cash"></i>
</button>
<?php endif; ?>
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" data-bs-boundary="window" aria-expanded="false" title="<?= h(tr('تغيير الحالة', 'Change status')) ?>">
@ -509,6 +525,9 @@ require __DIR__ . '/includes/header.php';
</div>
<a class="btn btn-sm btn-light border text-primary" href="<?= h(url_for('sale.php', ['id' => $order['id']])) ?>" title="<?= h(tr('تفاصيل', 'Detail')) ?>"><i class="bi bi-eye"></i></a>
<a class="btn btn-sm btn-outline-secondary" href="<?= h(url_for('edit_sale.php', ['id' => $order['id']])) ?>" title="<?= h(tr('تعديل', 'Edit')) ?>"><i class="bi bi-pencil"></i></a>
<button type="button" class="btn btn-sm btn-outline-danger" onclick='confirmDeleteEidOrder(<?= (int) $order['id'] ?>, <?= json_encode((string) ($order['receipt_no'] ?? '')) ?>)' title="<?= h(tr('حذف', 'Delete')) ?>">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
@ -531,39 +550,154 @@ require __DIR__ . '/includes/header.php';
<?php endif; ?>
</section>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.js-confirm-mark-paid').forEach(function (form) {
form.addEventListener('submit', function (event) {
event.preventDefault();
function formatPaymentPopupAmount(value) {
return Number(value || 0).toFixed(3);
}
var message = form.getAttribute('data-confirm-message') || '';
var title = form.getAttribute('data-confirm-title') || '';
async function confirmDeleteEidOrder(id, receiptNo) {
const displayReceiptNo = receiptNo || '#'+id;
const result = await Swal.fire({
icon: 'warning',
title: '<?= h(tr('حذف طلب العيد', 'Delete Eid order')) ?>',
text: '<?= h(tr('سيتم حذف الطلب نهائياً من القائمة. هل تريد المتابعة؟', 'This will permanently remove the order from the list. Do you want to continue?')) ?>' + ' (' + displayReceiptNo + ')',
showCancelButton: true,
confirmButtonColor: '#dc3545',
confirmButtonText: '<?= h(tr('نعم، احذف', 'Yes, delete')) ?>',
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>'
});
if (typeof Swal === 'undefined') {
if (window.confirm(message)) {
form.submit();
}
if (!result.isConfirmed) {
return;
}
const form = document.createElement('form');
form.method = 'post';
form.action = '';
const fields = {
action: 'delete_order',
id: String(id)
};
Object.entries(fields).forEach(([name, value]) => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = name;
input.value = value;
form.appendChild(input);
});
const query = window.location.search ? window.location.search.substring(1) : '';
if (query) {
query.split('&').forEach((pair) => {
if (!pair) {
return;
}
Swal.fire({
icon: 'warning',
title: title,
text: message,
position: 'center',
showCancelButton: true,
confirmButtonText: '<?= h(tr('نعم، تحويلها لمدفوع', 'Yes, mark it as paid')) ?>',
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>',
confirmButtonColor: '#198754',
cancelButtonColor: '#6c757d',
focusCancel: true
}).then(function (result) {
if (result.isConfirmed) {
form.submit();
}
});
const parts = pair.split('=');
const name = decodeURIComponent(parts[0] || '').trim();
if (!name || name === 'action' || name === 'id') {
return;
}
const input = document.createElement('input');
input.type = 'hidden';
input.name = name;
input.value = decodeURIComponent((parts[1] || '').replace(/\+/g, ' '));
form.appendChild(input);
});
}
document.body.appendChild(form);
form.submit();
}
async function receivePayment(id, totalAmount, paidAmount, dueAmount, completeOrder = false) {
const popupHtml = `
<div class="text-start">
<div class="row g-2 mb-3">
<div class="col-4">
<div class="border rounded-3 p-2 h-100 bg-light">
<div class="small text-muted"><?= h(tr('إجمالي الفاتورة', 'Total amount')) ?></div>
<div class="fw-bold text-dark">${formatPaymentPopupAmount(totalAmount)}</div>
</div>
</div>
<div class="col-4">
<div class="border rounded-3 p-2 h-100 bg-light">
<div class="small text-muted"><?= h(tr('المدفوع سابقاً', 'Already paid')) ?></div>
<div class="fw-bold text-primary">${formatPaymentPopupAmount(paidAmount)}</div>
</div>
</div>
<div class="col-4">
<div class="border rounded-3 p-2 h-100 bg-light">
<div class="small text-muted"><?= h(tr('المتبقي الحالي', 'Current remaining')) ?></div>
<div class="fw-bold text-danger">${formatPaymentPopupAmount(dueAmount)}</div>
</div>
</div>
</div>
<label for="swal-payment-amount" class="form-label fw-semibold mb-2"><?= h(tr('المبلغ المطلوب دفعه الآن', 'Amount to pay now')) ?></label>
<input id="swal-payment-amount" type="number" class="swal2-input mt-0" min="0.001" step="0.001" max="${formatPaymentPopupAmount(dueAmount)}" value="${formatPaymentPopupAmount(dueAmount)}">
<div class="d-flex justify-content-between align-items-center rounded-3 border px-3 py-2 bg-light mt-3">
<span class="small text-muted"><?= h(tr('المتبقي بعد الدفعة', 'Remaining after payment')) ?></span>
<strong id="swal-payment-remaining" class="text-success">0.000</strong>
</div>
</div>`;
const { isConfirmed, value: paymentAmount } = await Swal.fire({
title: '<?= h(tr('استلام دفعة', 'Receive Payment')) ?>',
html: popupHtml,
showCancelButton: true,
confirmButtonColor: '#198754',
confirmButtonText: '<?= h(tr('حفظ الدفعة', 'Save Payment')) ?>',
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>',
focusConfirm: false,
didOpen: () => {
const input = document.getElementById('swal-payment-amount');
const remainingEl = document.getElementById('swal-payment-remaining');
const updateRemaining = () => {
const amount = parseFloat(input.value || '0');
const safeAmount = Number.isFinite(amount) ? amount : 0;
const remaining = Math.max(dueAmount - safeAmount, 0);
remainingEl.textContent = formatPaymentPopupAmount(remaining);
remainingEl.className = remaining > 0.0005 ? 'text-danger' : 'text-success';
};
input.addEventListener('input', updateRemaining);
input.focus();
input.select();
updateRemaining();
},
preConfirm: () => {
const input = document.getElementById('swal-payment-amount');
const amount = parseFloat(input.value || '0');
if (!amount || amount <= 0) {
Swal.showValidationMessage('<?= h(tr('أدخل مبلغاً صحيحاً.', 'Enter a valid amount.')) ?>');
return false;
}
if (amount - dueAmount > 0.0005) {
Swal.showValidationMessage('<?= h(tr('المبلغ لا يمكن أن يتجاوز المتبقي.', 'Amount cannot exceed the due balance.')) ?>');
return false;
}
return formatPaymentPopupAmount(amount);
}
});
});
if (!isConfirmed || !paymentAmount) {
return;
}
const formData = new FormData();
formData.append('sale_id', String(id));
formData.append('payment_amount', String(paymentAmount));
if (completeOrder) {
formData.append('complete_order', '1');
}
const response = await fetch('api/sales_payment.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.success) {
await Swal.fire({ icon: 'success', text: data.message, confirmButtonText: 'OK' });
window.location.reload();
} else {
Swal.fire({ icon: 'error', text: data.error || '<?= h(tr('تعذر تسجيل الدفعة.', 'Could not record the payment.')) ?>' });
}
}
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>

View File

@ -30,6 +30,12 @@ $notesPlaceholder = $isEidOrder
$notesHelper = $isEidOrder
? tr('هذه الملاحظة داخلية وتبقى في النظام فقط، ولن تظهر في الإيصال المطبوع.', 'This note is internal, stays in the system only, and will not appear on the printed receipt.')
: tr('سيتم حفظ هذه الملاحظات مع الفاتورة داخل النظام.', 'These notes will be saved with the invoice inside the system.');
$itemNotePlaceholder = $isEidOrder
? tr('مثال: بدون سكر، تغليف هدية، لون معين، كتابة اسم...', 'Example: no sugar, gift wrap, specific color, write a name...')
: tr('ملاحظة داخلية لهذا الصنف...', 'Internal note for this item...');
$itemNoteHelper = $isEidOrder
? tr('ملاحظة داخلية لهذا الصنف فقط — لا تُطبع في الإيصال.', 'Internal for this item only — not printed on the receipt.')
: tr('ملاحظة محفوظة لهذا الصنف داخل النظام.', 'This note is saved for this item inside the system.');
try {
$customers = db()->query('SELECT id, name, phone FROM customers ORDER BY name ASC')->fetchAll();
@ -97,6 +103,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$finalName = $sku;
}
$itemNote = trim((string) ($item['item_note'] ?? ''));
$price = isset($item['price']) && is_numeric($item['price'])
? max(0, (float) $item['price'])
: (float) $product['price'];
@ -113,6 +121,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'name_en' => $finalName,
'qty' => $qty,
'price' => $price,
'item_note' => $itemNote,
'line_total' => $lineTotal,
'vat_percent' => $vatPercent,
'vat_amount' => $itemVat
@ -604,6 +613,9 @@ require __DIR__ . '/header.php';
const catalogData = <?= json_encode($catalog, JSON_UNESCAPED_UNICODE) ?>;
const catalogArray = Object.values(catalogData);
const partialPaymentText = '<?= h(tr('متبقٍ بعد الحفظ:', 'Due after save:')) ?>';
const showItemNotes = <?= $isEidOrder ? 'true' : 'false' ?>;
const itemNotePlaceholder = <?= json_encode($itemNotePlaceholder, JSON_UNESCAPED_UNICODE) ?>;
const itemNoteHelper = <?= json_encode($itemNoteHelper, JSON_UNESCAPED_UNICODE) ?>;
let invoiceItems = {};
let currentInvoiceTotal = 0;
@ -780,7 +792,8 @@ function addItemToInvoice(sku) {
sku: sku,
name: '<?= current_lang() ?>' === 'ar' ? item.name_ar : item.name_en,
price: parseFloat(item.price),
qty: 1
qty: 1,
item_note: ''
};
}
renderInvoice();
@ -809,6 +822,23 @@ function changeItemPrice(sku, newPrice) {
renderInvoice();
}
function changeItemNoteValue(sku, newNote) {
if (!invoiceItems[sku]) {
return;
}
invoiceItems[sku].item_note = String(newNote ?? '');
cartJson.value = JSON.stringify(Object.keys(invoiceItems).map(currentSku => {
const currentItem = invoiceItems[currentSku];
return {
sku: currentItem.sku,
name: currentItem.name,
price: currentItem.price,
qty: currentItem.qty,
item_note: String(currentItem.item_note ?? '')
};
}));
}
function changeQty(sku, newQty) {
const qty = parseInt(newQty);
if (isNaN(qty) || qty < 1) {
@ -854,16 +884,22 @@ function renderInvoice() {
totalVat += itemVat;
totalAmount += lineTotal;
cartData.push({ sku: item.sku, name: item.name, price: item.price, qty: item.qty });
cartData.push({ sku: item.sku, name: item.name, price: item.price, qty: item.qty, item_note: String(item.item_note || '') });
const safeSku = escapeHtml(item.sku);
const safeName = escapeHtml(item.name);
const safeItemNote = escapeHtml(item.item_note || '');
const itemNoteHtml = showItemNotes ? `
<input type="text" class="form-control form-control-sm line-input mt-2" value="${safeItemNote}" oninput="changeItemNoteValue('${sku}', this.value)" placeholder="${escapeHtml(itemNotePlaceholder)}">
<div class="text-muted small mt-1">${escapeHtml(itemNoteHelper)}</div>
` : '';
const tr = document.createElement('tr');
tr.innerHTML = `
<td>
<input type="text" class="form-control form-control-sm line-input fw-medium mb-2" value="${safeName}" onchange="changeItemName('${sku}', this.value)" onkeyup="if(event.key==='Enter') changeItemName('${sku}', this.value)">
<div class="text-muted small">SKU: ${safeSku}</div>
${itemNoteHtml}
<div class="text-muted small mt-2">SKU: ${safeSku}</div>
</td>
<td class="text-center align-middle">
<input type="number" class="form-control form-control-sm text-center line-input line-price-input mx-auto" min="0" step="0.001" value="${item.price.toFixed(3)}" onchange="changeItemPrice('${sku}', this.value)" onkeyup="if(event.key==='Enter') changeItemPrice('${sku}', this.value)">

View File

@ -330,7 +330,7 @@ require __DIR__ . '/includes/header.php';
<td><?= h(date('Y-m-d H:i', strtotime((string) $sale['sale_date']))) ?></td>
<td>
<?php if ($paymentSummary['due_amount'] > 0.0005): ?>
<button class="btn btn-sm btn-outline-success rounded-circle shadow-sm me-1" style="width: 34px; height: 34px; padding: 0;" onclick="receivePayment(<?= (int) $sale['id'] ?>, <?= json_encode((float) $paymentSummary['due_amount']) ?>, <?= ($sale['status'] ?? 'completed') === 'order' ? 'true' : 'false' ?>)" title="<?= h(tr('استلام دفعة', 'Receive Payment')) ?>">
<button class="btn btn-sm btn-outline-success rounded-circle shadow-sm me-1" style="width: 34px; height: 34px; padding: 0;" onclick="receivePayment(<?= (int) $sale['id'] ?>, <?= json_encode((float) $sale['total_amount']) ?>, <?= json_encode((float) $paymentSummary['paid_amount']) ?>, <?= json_encode((float) $paymentSummary['due_amount']) ?>, <?= ($sale['status'] ?? 'completed') === 'order' ? 'true' : 'false' ?>)" title="<?= h(tr('استلام دفعة', 'Receive Payment')) ?>">
<i class="bi bi-cash-coin"></i>
</button>
<?php endif; ?>
@ -365,30 +365,80 @@ require __DIR__ . '/includes/header.php';
</section>
<script>
async function receivePayment(id, dueAmount, completeOrder = false) {
const { value: paymentAmount } = await Swal.fire({
function formatPaymentPopupAmount(value) {
return Number(value || 0).toFixed(3);
}
async function receivePayment(id, totalAmount, paidAmount, dueAmount, completeOrder = false) {
const popupHtml = `
<div class="text-start">
<div class="row g-2 mb-3">
<div class="col-4">
<div class="border rounded-3 p-2 h-100 bg-light">
<div class="small text-muted"><?= h(tr('إجمالي الفاتورة', 'Total amount')) ?></div>
<div class="fw-bold text-dark">${formatPaymentPopupAmount(totalAmount)}</div>
</div>
</div>
<div class="col-4">
<div class="border rounded-3 p-2 h-100 bg-light">
<div class="small text-muted"><?= h(tr('المدفوع سابقاً', 'Already paid')) ?></div>
<div class="fw-bold text-primary">${formatPaymentPopupAmount(paidAmount)}</div>
</div>
</div>
<div class="col-4">
<div class="border rounded-3 p-2 h-100 bg-light">
<div class="small text-muted"><?= h(tr('المتبقي الحالي', 'Current remaining')) ?></div>
<div class="fw-bold text-danger">${formatPaymentPopupAmount(dueAmount)}</div>
</div>
</div>
</div>
<label for="swal-payment-amount" class="form-label fw-semibold mb-2"><?= h(tr('المبلغ المطلوب دفعه الآن', 'Amount to pay now')) ?></label>
<input id="swal-payment-amount" type="number" class="swal2-input mt-0" min="0.001" step="0.001" max="${formatPaymentPopupAmount(dueAmount)}" value="${formatPaymentPopupAmount(dueAmount)}">
<div class="d-flex justify-content-between align-items-center rounded-3 border px-3 py-2 bg-light mt-3">
<span class="small text-muted"><?= h(tr('المتبقي بعد الدفعة', 'Remaining after payment')) ?></span>
<strong id="swal-payment-remaining" class="text-success">0.000</strong>
</div>
</div>`;
const { isConfirmed, value: paymentAmount } = await Swal.fire({
title: '<?= h(tr('استلام دفعة', 'Receive Payment')) ?>',
text: '<?= h(tr('أدخل المبلغ المستلم لهذه الفاتورة.', 'Enter the amount received for this invoice.')) ?>',
input: 'number',
inputAttributes: { min: '0.001', step: '0.001', max: String(dueAmount) },
inputValue: Number(dueAmount).toFixed(3),
html: popupHtml,
showCancelButton: true,
confirmButtonColor: '#198754',
confirmButtonText: '<?= h(tr('حفظ الدفعة', 'Save Payment')) ?>',
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>',
inputValidator: (value) => {
const amount = parseFloat(value || '0');
focusConfirm: false,
didOpen: () => {
const input = document.getElementById('swal-payment-amount');
const remainingEl = document.getElementById('swal-payment-remaining');
const updateRemaining = () => {
const amount = parseFloat(input.value || '0');
const safeAmount = Number.isFinite(amount) ? amount : 0;
const remaining = Math.max(dueAmount - safeAmount, 0);
remainingEl.textContent = formatPaymentPopupAmount(remaining);
remainingEl.className = remaining > 0.0005 ? 'text-danger' : 'text-success';
};
input.addEventListener('input', updateRemaining);
input.focus();
input.select();
updateRemaining();
},
preConfirm: () => {
const input = document.getElementById('swal-payment-amount');
const amount = parseFloat(input.value || '0');
if (!amount || amount <= 0) {
return '<?= h(tr('أدخل مبلغاً صحيحاً.', 'Enter a valid amount.')) ?>';
Swal.showValidationMessage('<?= h(tr('أدخل مبلغاً صحيحاً.', 'Enter a valid amount.')) ?>');
return false;
}
if (amount - dueAmount > 0.0005) {
return '<?= h(tr('المبلغ لا يمكن أن يتجاوز المتبقي.', 'Amount cannot exceed the due balance.')) ?>';
Swal.showValidationMessage('<?= h(tr('المبلغ لا يمكن أن يتجاوز المتبقي.', 'Amount cannot exceed the due balance.')) ?>');
return false;
}
return null;
return formatPaymentPopupAmount(amount);
}
});
if (!paymentAmount) {
if (!isConfirmed || !paymentAmount) {
return;
}