new updates
This commit is contained in:
parent
8df558e09d
commit
ba54d3065c
76
debts.php
76
debts.php
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 ({'&': '&', '<': '<', '>': '>', '"': '"', "'": '''})[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">
|
||||
|
||||
204
eid_orders.php
204
eid_orders.php
@ -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'; ?>
|
||||
|
||||
@ -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)">
|
||||
|
||||
76
sales.php
76
sales.php
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user