index again

This commit is contained in:
Flatlogic Bot 2026-05-03 12:44:17 +00:00
parent 4034cc3c7f
commit c23bc66b9c
7 changed files with 1275 additions and 228 deletions

View File

@ -2106,20 +2106,73 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
exit;
}
if ($action === 'get_invoice_details') {
header('Content-Type: application/json');
$invoice_id = (int)($_GET['invoice_id'] ?? 0);
$type = (($_GET['type'] ?? 'sale') === 'purchase') ? 'purchase' : 'sale';
if ($invoice_id < 1) {
echo json_encode(['success' => false, 'error' => 'Invalid invoice id']);
exit;
}
$table = ($type === 'purchase') ? 'purchases' : 'invoices';
$itemTable = ($type === 'purchase') ? 'purchase_items' : 'invoice_items';
$fkColumn = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
$partyColumn = ($type === 'purchase') ? 'supplier_id' : 'customer_id';
$partyTable = ($type === 'purchase') ? 'suppliers' : 'customers';
$partyAlias = ($type === 'purchase') ? 'supplier_name' : 'customer_name';
$where = ['v.id = ?'];
$params = [$invoice_id];
if (db_column_exists($table, 'outlet_id')) {
$oid = current_outlet_id();
if ($oid !== -1) {
$where[] = '(v.outlet_id = ? OR v.outlet_id IS NULL)';
$params[] = $oid;
}
}
$stmt = db()->prepare("SELECT v.*, c.name AS {$partyAlias}, c.phone AS party_phone FROM $table v LEFT JOIN $partyTable c ON v.$partyColumn = c.id WHERE " . implode(' AND ', $where) . " LIMIT 1");
$stmt->execute($params);
$invoice = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$invoice) {
echo json_encode(['success' => false, 'error' => 'Invoice not found']);
exit;
}
$partyName = trim((string)($invoice[$partyAlias] ?? ''));
if ($partyName === '' && $type === 'sale' && !empty($invoice['is_pos'])) {
$partyName = 'Walk-in Customer';
}
$invoice['type'] = $type;
$invoice['party_name'] = $partyName !== '' ? $partyName : '---';
$invoice['paid_amount'] = (float)($invoice['paid_amount'] ?? 0);
$stmtItems = db()->prepare("SELECT li.*, i.name_en, i.name_ar, i.sku, i.vat_rate, i.stock_quantity FROM $itemTable li LEFT JOIN stock_items i ON li.item_id = i.id WHERE li.$fkColumn = ?");
$stmtItems->execute([$invoice_id]);
$invoice['items'] = $stmtItems->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($invoice, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
exit;
}
if ($action === 'get_invoice_items') {
header('Content-Type: application/json');
$invoice_id = (int)$_GET['invoice_id'];
$type = $_GET['type'] ?? 'sale';
if ($type === 'purchase') {
$stmt = db()->prepare("SELECT pi.*, i.name_en, i.name_ar, i.sku
$stmt = db()->prepare("SELECT pi.*, i.name_en, i.name_ar, i.sku, i.vat_rate, i.stock_quantity
FROM purchase_items pi
JOIN stock_items i ON pi.item_id = i.id
LEFT JOIN stock_items i ON pi.item_id = i.id
WHERE pi.purchase_id = ?");
} else {
$stmt = db()->prepare("SELECT ii.*, i.name_en, i.name_ar, i.sku
$stmt = db()->prepare("SELECT ii.*, i.name_en, i.name_ar, i.sku, i.vat_rate, i.stock_quantity
FROM invoice_items ii
JOIN stock_items i ON ii.item_id = i.id
LEFT JOIN stock_items i ON ii.item_id = i.id
WHERE ii.invoice_id = ?");
}
$stmt->execute([$invoice_id]);

View File

@ -1,223 +1,148 @@
// Edit Invoice Logic
document.querySelectorAll('.edit-invoice-btn').forEach(btn => {
btn.addEventListener('click', function() {
const data = JSON.parse(this.dataset.json);
document.getElementById('edit_invoice_id').value = data.id;
document.getElementById('edit_customer_id').value = data.customer_id;
document.getElementById('edit_invoice_date').value = data.invoice_date;
document.getElementById('edit_due_date').value = data.due_date || '';
document.getElementById('edit_payment_type').value = data.payment_type || 'cash';
document.getElementById('edit_status').value = data.status || 'unpaid';
document.getElementById('edit_paid_amount').value = parseFloat(data.paid_amount || 0).toFixed(3);
if (data.status === 'partially_paid') {
document.getElementById('editPaidAmountContainer').style.display = 'block';
} else {
document.getElementById('editPaidAmountContainer').style.display = 'none';
}
const tableBody = document.getElementById('editInvoiceItemsTableBody');
tableBody.innerHTML = '';
data.items.forEach(item => {
// We need more data than what's in invoice_items (like SKU and names, but we have them from the join in PHP)
// The dataset-json already contains name_en, name_ar etc because of the PHP logic at line 1093
const itemMeta = {
id: item.item_id,
name_en: item.name_en,
name_ar: item.name_ar,
sku: '', // Optional, or fetch if needed
vat_rate: 0 // Will be handled if we have it in the join
};
const parseInvoiceButtonPayload = (btn) => {
if (!btn || !btn.dataset || !btn.dataset.json) return {};
try {
return JSON.parse(btn.dataset.json);
} catch (error) {
console.warn('Failed to parse invoice payload from button data.', error);
return {};
}
};
// Fetch current item details to get VAT rate if possible, or use stored if available
// For simplicity, let's assume we want to use the item's current VAT rate or store it.
// Looking at the join at line 1093, it doesn't fetch vat_rate. Let's fix that in PHP too.
addItemToTable({
id: item.item_id,
name_en: item.name_en,
name_ar: item.name_ar,
sku: '',
vat_rate: item.vat_rate || 0 // We'll add this to PHP join
}, tableBody, null, null,
document.getElementById('edit_grandTotal'),
document.getElementById('edit_subtotal'),
document.getElementById('edit_totalVat'),
{ quantity: item.quantity, unit_price: item.unit_price });
const syncSelect2Value = (select) => {
if (!select) return;
if (select.classList.contains('select2') && window.jQuery && jQuery.fn && jQuery.fn.select2) {
jQuery(select).trigger('change');
}
};
const ensureSelectOption = (selectId, value, label = '') => {
const select = document.getElementById(selectId);
if (!select || value === null || value === undefined || String(value) === '') return;
const alreadyExists = Array.from(select.options || []).some(option => option.value == String(value));
if (!alreadyExists) {
const option = document.createElement('option');
option.value = String(value);
option.textContent = label || String(value);
select.appendChild(option);
}
};
const setBlankSelectOptionLabel = (select, label = '---') => {
if (!select) return;
const emptyOption = Array.from(select.options || []).find(option => option.value === '');
if (emptyOption) {
emptyOption.textContent = label;
}
};
const normalizeEditPaymentType = (paymentType) => {
let normalized = String(paymentType || 'cash').toLowerCase().replace(/[\s-]+/g, '_');
if (normalized === 'pos') normalized = 'cash';
if (!['cash', 'card', 'bank_transfer', 'credit'].includes(normalized)) {
normalized = 'cash';
}
return normalized;
};
const renderEditInvoiceItems = (items) => {
const tableBody = document.getElementById('editInvoiceItemsTableBody');
const grandTotalEl = document.getElementById('edit_grandTotal');
const subtotalEl = document.getElementById('edit_subtotal');
const totalVatEl = document.getElementById('edit_totalVat');
if (!tableBody) return;
tableBody.innerHTML = '';
if (!Array.isArray(items) || items.length === 0) {
if (typeof recalculate === 'function') {
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
}
return;
}
items.forEach(item => {
addItemToTable({
id: item.item_id || item.id,
name_en: item.name_en || item.item_name_en || 'Item',
name_ar: item.name_ar || item.item_name_ar || '',
sku: item.sku || '',
vat_rate: item.vat_rate || 0,
stock_quantity: item.stock_quantity || 0
}, tableBody, null, null, grandTotalEl, subtotalEl, totalVatEl, {
quantity: item.quantity,
unit_price: item.unit_price
});
});
};
const populateEditInvoiceModal = (data) => {
if (!data) return;
const invoiceIdInput = document.getElementById('edit_invoice_id');
const partySelect = document.getElementById('edit_customer_id');
const invoiceDateInput = document.getElementById('edit_invoice_date');
const dueDateInput = document.getElementById('edit_due_date');
const paymentTypeSelect = document.getElementById('edit_payment_type');
const statusSelect = document.getElementById('edit_status');
const paidAmountInput = document.getElementById('edit_paid_amount');
const paidAmountContainer = document.getElementById('editPaidAmountContainer');
const partyId = data.customer_id ?? data.supplier_id ?? '';
const partyLabel = data.party_name || data.customer_name || data.supplier_name || '';
ensureSelectOption('edit_customer_id', partyId, partyLabel);
if (invoiceIdInput) invoiceIdInput.value = data.id || '';
if (partySelect) {
setBlankSelectOptionLabel(partySelect, (partyId === '' && partyLabel) ? partyLabel : '---');
partySelect.value = partyId;
syncSelect2Value(partySelect);
}
if (invoiceDateInput) invoiceDateInput.value = data.invoice_date || '';
if (dueDateInput) dueDateInput.value = data.due_date || '';
if (paymentTypeSelect) paymentTypeSelect.value = normalizeEditPaymentType(data.payment_type);
if (statusSelect) statusSelect.value = data.status || 'unpaid';
if (paidAmountInput) paidAmountInput.value = parseFloat(data.paid_amount || 0).toFixed(3);
if (paidAmountContainer) {
paidAmountContainer.style.display = data.status === 'partially_paid' ? 'block' : 'none';
}
renderEditInvoiceItems(data.items || []);
};
document.querySelectorAll('.edit-invoice-btn').forEach(btn => {
btn.addEventListener('click', async function() {
const fallbackData = parseInvoiceButtonPayload(this);
const invoiceId = this.dataset.id || fallbackData.id || '';
const type = this.dataset.type || fallbackData.type || invoiceType || 'sale';
const tableBody = document.getElementById('editInvoiceItemsTableBody');
if (Object.keys(fallbackData).length > 0) {
populateEditInvoiceModal(fallbackData);
} else if (tableBody) {
tableBody.innerHTML = '<tr><td colspan="6" class="text-center"><div class="spinner-border spinner-border-sm text-primary"></div> <span data-en="Loading invoice..." data-ar="جاري تحميل الفاتورة...">Loading invoice...</span></td></tr>';
}
if (!invoiceId) return;
try {
const resp = await fetch(`index.php?action=get_invoice_details&invoice_id=${encodeURIComponent(invoiceId)}&type=${encodeURIComponent(type)}`);
const data = await resp.json();
if (data && data.id) {
populateEditInvoiceModal(data);
} else if (data && data.error) {
throw new Error(data.error);
}
} catch (error) {
console.error('Failed to load invoice details for edit modal.', error);
if (window.Swal) {
Swal.fire('Error', 'Failed to load invoice details', 'error');
}
}
});
});
// View and Print Invoice Logic
document.addEventListener('click', function(e) {
if (e.target.closest('.view-invoice-btn')) {
const btn = e.target.closest('.view-invoice-btn');
const data = JSON.parse(btn.dataset.json);
if (window.viewAndPrintA4Invoice) {
window.viewAndPrintA4Invoice(data, false);
}
}
if (e.target.closest('.print-a4-btn')) {
const btn = e.target.closest('.print-a4-btn');
const data = JSON.parse(btn.dataset.json);
if (window.viewAndPrintA4Invoice) {
window.viewAndPrintA4Invoice(data, true);
}
}
});
// Return Logic (General for Sales and Purchase)
const setupReturnLogic = (selectId, containerId, tbodyId, totalDisplayId, submitBtnId, type = "sale") => {
const select = document.getElementById(selectId);
if (!select) return;
const calculateTotal = function() {
let total = 0;
document.querySelectorAll('#' + tbodyId + ' tr').forEach(row => {
const qtyInput = row.querySelector('.return-qty-input');
if (!qtyInput) return;
const qty = parseFloat(qtyInput.value) || 0;
const price = parseFloat(qtyInput.dataset.price) || 0;
const lineTotal = qty * price;
const lineTotalDisplay = row.querySelector('.line-total');
if (lineTotalDisplay) {
lineTotalDisplay.innerText = lineTotal.toFixed(3);
}
total += lineTotal;
});
const totalDisplay = document.getElementById(totalDisplayId);
if (totalDisplay) {
totalDisplay.innerText = 'OMR ' + total.toFixed(3);
}
const submitBtn = document.getElementById(submitBtnId);
if (submitBtn) {
submitBtn.disabled = total <= 0;
}
};
const handleInvoiceChange = async function() {
const invoiceId = select.value;
const container = document.getElementById(containerId);
const tbody = document.getElementById(tbodyId);
const submitBtn = document.getElementById(submitBtnId);
if (!invoiceId) {
if (container) container.style.display = 'none';
if (submitBtn) submitBtn.disabled = true;
return;
}
if (tbody) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center"><div class="spinner-border spinner-border-sm text-primary"></div> <span data-en="Loading items..." data-ar="جاري تحميل الأصناف...">Loading items...</span></td></tr>';
}
if (container) container.style.display = 'block';
try {
const resp = await fetch(`index.php?action=get_invoice_items&invoice_id=${invoiceId}&type=${type}`);
const items = await resp.json();
if (tbody) {
tbody.innerHTML = '';
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted" data-en="No items found for this invoice." data-ar="لا توجد أصناف لهذه الفاتورة.">No items found for this invoice.</td></tr>';
} else {
let html = '';
items.forEach(item => {
html += `
<tr>
<td>${item.name_en}<br><small class="text-muted">${item.sku}</small></td>
<td>${parseFloat(item.quantity).toFixed(2)}</td>
<td>
<input type="number" name="quantities[]" class="form-control form-control-sm return-qty-input"
step="0.01" min="0" max="${item.quantity}" value="0"
data-price="${item.unit_price}">
<input type="hidden" name="item_ids[]" value="${item.item_id}">
<input type="hidden" name="prices[]" value="${item.unit_price}">
</td>
<td class="text-end">${parseFloat(item.unit_price).toFixed(3)}</td>
<td class="text-end line-total">0.000</td>
</tr>
`;
});
tbody.innerHTML = html;
}
}
if (submitBtn) submitBtn.disabled = true;
const qtyInputs = tbody.querySelectorAll('.return-qty-input');
qtyInputs.forEach(input => {
['input', 'change', 'keyup'].forEach(evt => input.addEventListener(evt, calculateTotal));
});
calculateTotal();
} catch (e) {
console.error(e);
if (window.Swal) Swal.fire('Error', 'Failed to fetch invoice items', 'error');
}
};
select.addEventListener('change', handleInvoiceChange);
if (window.jQuery && jQuery.fn.select2) {
$(select).on('select2:select change', handleInvoiceChange);
}
};
setupReturnLogic('return_invoice_select', 'return_items_container', 'return_items_tbody', 'return_total_display', 'submit_return_btn', 'sale');
setupReturnLogic('purchase_return_invoice_select', 'purchase_return_items_container', 'purchase_return_items_tbody', 'purchase_return_total_display', 'purchase_submit_return_btn', 'purchase');
// Return Invoice Button from Sales/Purchases list
document.querySelectorAll('.return-invoice-btn').forEach(btn => {
btn.addEventListener('click', function() {
const invoiceId = this.dataset.id;
const targetModal = this.dataset.bsTarget;
const selectId = targetModal === '#addSalesReturnModal' ? 'return_invoice_select' : 'purchase_return_invoice_select';
const select = document.getElementById(selectId);
if (select) {
$(select).val(invoiceId).trigger('change');
}
});
});
// View Return Logic
document.querySelectorAll('.view-return-btn').forEach(btn => {
btn.addEventListener('click', async function() {
const returnId = this.dataset.id;
const type = '<?= $page === "purchase_returns" ? "purchase" : "sale" ?>';
const modal = new bootstrap.Modal(document.getElementById('viewReturnDetailsModal'));
try {
const resp = await fetch(`index.php?action=get_return_details&return_id=${returnId}&type=${type}`);
const data = await resp.json();
if (data) {
document.getElementById('view_return_no').innerText = (type === 'purchase' ? 'PRET-' : 'SRET-') + String(data.id).padStart(5, '0');
document.getElementById('view_return_party').innerText = data.party_name;
document.getElementById('view_return_date').innerText = data.return_date;
const refId = data.purchase_id || data.invoice_id;
const refPrefix = type === 'purchase' ? 'PUR-' : 'INV-';
document.getElementById('view_return_invoice').innerText = refPrefix + String(refId).padStart(5, '0');
document.getElementById('view_return_total').innerText = 'OMR ' + parseFloat(data.total_amount).toFixed(3);
document.getElementById('view_return_notes').innerText = data.notes || 'No notes';
let itemsHtml = '';
data.items.forEach(item => {
itemsHtml += `
<tr>
<td>${item.name_en}<br><small class="text-muted">${item.sku}</small></td>
<td class="text-center">${parseFloat(item.quantity).toFixed(2)}</td>
<td class="text-end">${parseFloat(item.unit_price).toFixed(3)}</td>
<td class="text-end">${parseFloat(item.total_price).toFixed(3)}</td>
</tr>
`;
});
document.getElementById('view_return_items_tbody').innerHTML = itemsHtml;
modal.show();
}
} catch (e) {
console.error(e);
Swal.fire('Error', 'Failed to fetch return details', 'error');
}
});
});

View File

@ -130,11 +130,11 @@
$inv['total_in_words'] = numberToWordsOMR($inv['total_with_vat']);
if ($type === 'sale') {
$item_stmt = db()->prepare("SELECT ii.*, i.name_en, i.name_ar, i.vat_rate FROM invoice_items ii LEFT JOIN stock_items i ON ii.item_id = i.id WHERE ii.invoice_id = ?");
$item_stmt = db()->prepare("SELECT ii.*, i.name_en, i.name_ar, i.sku, i.vat_rate, i.stock_quantity FROM invoice_items ii LEFT JOIN stock_items i ON ii.item_id = i.id WHERE ii.invoice_id = ?");
$item_stmt->execute([$inv['id']]);
$inv['items'] = $item_stmt->fetchAll(PDO::FETCH_ASSOC);
} else {
$item_stmt = db()->prepare("SELECT pi.*, i.name_en, i.name_ar, i.vat_rate FROM purchase_items pi LEFT JOIN stock_items i ON pi.item_id = i.id WHERE pi.purchase_id = ?");
$item_stmt = db()->prepare("SELECT pi.*, i.name_en, i.name_ar, i.sku, i.vat_rate, i.stock_quantity FROM purchase_items pi LEFT JOIN stock_items i ON pi.item_id = i.id WHERE pi.purchase_id = ?");
$item_stmt->execute([$inv['id']]);
$inv['items'] = $item_stmt->fetchAll(PDO::FETCH_ASSOC);
}

View File

@ -519,7 +519,7 @@
<div class="row g-3 mb-4">
<div class="col-md-3">
<label class="form-label fw-bold" data-en="<?= $page === 'sales' ? 'Customer' : 'Supplier' ?>" data-ar="<?= $page === 'sales' ? 'العميل' : 'المورد' ?>"><?= $page === 'sales' ? 'Customer' : 'Supplier' ?></label>
<select name="customer_id" id="edit_customer_id" class="form-select select2" required>
<select name="customer_id" id="edit_customer_id" class="form-select select2" <?= $page === 'sales' ? '' : 'required' ?>>
<option value="">---</option>
<?php foreach ($data['customers_list'] as $c): ?>
<option value="<?= $c['id'] ?>"><?= htmlspecialchars($c['name']) ?></option>

View File

@ -12,7 +12,8 @@
$cust_supplier_col = ($type === 'purchase') ? 'supplier_id' : 'customer_id';
$fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
$cust_id = (int)$_POST['customer_id'];
$rawCustomerId = $_POST['customer_id'] ?? '';
$cust_id = ($type === 'sale' && ($rawCustomerId === '' || $rawCustomerId === null)) ? null : (int)$rawCustomerId;
$inv_date = $_POST['invoice_date'] ?: date('Y-m-d');
$due_date = $_POST['due_date'] ?: null;
$status = $_POST['status'] ?? 'pending';
@ -111,7 +112,8 @@
$cust_supplier_col = ($type === 'purchase') ? 'supplier_id' : 'customer_id';
$fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
$cust_id = (int)$_POST['customer_id'];
$rawCustomerId = $_POST['customer_id'] ?? '';
$cust_id = ($type === 'sale' && ($rawCustomerId === '' || $rawCustomerId === null)) ? null : (int)$rawCustomerId;
$date = $_POST['invoice_date'] ?: date('Y-m-d');
$due_date = $_POST['due_date'] ?: null;
$status = $_POST['status'] ?? 'pending';

View File

@ -164,14 +164,15 @@
<td class="text-end text-success">OMR <?= number_format((float)$inv['paid_amount'], 3) ?></td>
<td class="text-end text-danger fw-bold">OMR <?= number_format((float)$inv['balance_amount'], 3) ?></td>
<td class="text-end d-print-none">
<?php $invoiceJson = (string)(json_encode($inv, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE) ?: '{}'); ?>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-info view-invoice-btn" data-json="<?= htmlspecialchars(json_encode($inv)) ?>" title="View"><i class="bi bi-eye"></i></button>
<button class="btn btn-outline-info view-invoice-btn" data-json="<?= htmlspecialchars($invoiceJson) ?>" title="View"><i class="bi bi-eye"></i></button>
<button class="btn btn-outline-warning return-invoice-btn" data-id="<?= $inv['id'] ?>" data-bs-toggle="modal" data-bs-target="<?= $page === 'sales' ? '#addSalesReturnModal' : '#addPurchaseReturnModal' ?>" title="Return"><i class="bi bi-arrow-return-left"></i></button>
<button class="btn btn-outline-primary edit-invoice-btn" data-json="<?= htmlspecialchars(json_encode($inv)) ?>" data-bs-toggle="modal" data-bs-target="#editInvoiceModal" title="Edit"><i class="bi bi-pencil"></i></button>
<button class="btn btn-outline-primary edit-invoice-btn" data-id="<?= $inv['id'] ?>" data-type="<?= htmlspecialchars($inv['type']) ?>" data-json="<?= htmlspecialchars($invoiceJson) ?>" data-bs-toggle="modal" data-bs-target="#editInvoiceModal" title="Edit"><i class="bi bi-pencil"></i></button>
<?php if ($inv['status'] !== 'paid'): ?>
<button class="btn btn-outline-success pay-invoice-btn" data-id="<?= $inv['id'] ?>" data-total="<?= $inv['total_with_vat'] ?>" data-paid="<?= $inv['paid_amount'] ?>" data-bs-toggle="modal" data-bs-target="#payInvoiceModal" title="Payment"><i class="bi bi-cash-coin"></i></button>
<?php endif; ?>
<button class="btn btn-outline-secondary print-a4-btn" data-json="<?= htmlspecialchars(json_encode($inv)) ?>" title="Print A4 Invoice"><i class="bi bi-printer"></i></button>
<button class="btn btn-outline-secondary print-a4-btn" data-json="<?= htmlspecialchars($invoiceJson) ?>" title="Print A4 Invoice"><i class="bi bi-printer"></i></button>
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this invoice?')">
<input type="hidden" name="id" value="<?= $inv['id'] ?>">
<button type="submit" name="delete_invoice" class="btn btn-outline-danger" title="Delete"><i class="bi bi-trash"></i></button>

1066
tmp_inline_script.js Normal file

File diff suppressed because it is too large Load Diff