Revert to version 5ee3913
This commit is contained in:
parent
f20efe1066
commit
8238050b60
693
index.php
693
index.php
@ -725,7 +725,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$customer_id = $_POST['customer_id'] ?: null;
|
$customer_id = $_POST['customer_id'] ?: null;
|
||||||
$quotation_date = $_POST['quotation_date'] ?: date('Y-m-d');
|
$quotation_date = $_POST['quotation_date'] ?: date('Y-m-d');
|
||||||
$valid_until = $_POST['valid_until'] ?: null;
|
$valid_until = $_POST['valid_until'] ?: null;
|
||||||
$terms_conditions = $_POST['terms_conditions'] ?? '';
|
|
||||||
$item_ids = $_POST['item_ids'] ?? [];
|
$item_ids = $_POST['item_ids'] ?? [];
|
||||||
$quantities = $_POST['quantities'] ?? [];
|
$quantities = $_POST['quantities'] ?? [];
|
||||||
$prices = $_POST['prices'] ?? [];
|
$prices = $_POST['prices'] ?? [];
|
||||||
@ -750,8 +749,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$items_data[] = ['id' => $item_id, 'qty' => $qty, 'price' => $price, 'total' => $line_total];
|
$items_data[] = ['id' => $item_id, 'qty' => $qty, 'price' => $price, 'total' => $line_total];
|
||||||
}
|
}
|
||||||
$total_with_vat = $subtotal + $total_vat;
|
$total_with_vat = $subtotal + $total_vat;
|
||||||
$stmt = $db->prepare("INSERT INTO quotations (customer_id, quotation_date, valid_until, total_amount, vat_amount, total_with_vat, terms_conditions) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
$stmt = $db->prepare("INSERT INTO quotations (customer_id, quotation_date, valid_until, total_amount, vat_amount, total_with_vat) VALUES (?, ?, ?, ?, ?, ?)");
|
||||||
$stmt->execute([$customer_id, $quotation_date, $valid_until, $subtotal, $total_vat, $total_with_vat, $terms_conditions]);
|
$stmt->execute([$customer_id, $quotation_date, $valid_until, $subtotal, $total_vat, $total_with_vat]);
|
||||||
$quotation_id = $db->lastInsertId();
|
$quotation_id = $db->lastInsertId();
|
||||||
foreach ($items_data as $item) {
|
foreach ($items_data as $item) {
|
||||||
$stmt = $db->prepare("INSERT INTO quotation_items (quotation_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
|
$stmt = $db->prepare("INSERT INTO quotation_items (quotation_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
|
||||||
@ -772,7 +771,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$quotation_date = $_POST['quotation_date'] ?: date('Y-m-d');
|
$quotation_date = $_POST['quotation_date'] ?: date('Y-m-d');
|
||||||
$valid_until = $_POST['valid_until'] ?: null;
|
$valid_until = $_POST['valid_until'] ?: null;
|
||||||
$status = $_POST['status'] ?? 'pending';
|
$status = $_POST['status'] ?? 'pending';
|
||||||
$terms_conditions = $_POST['terms_conditions'] ?? '';
|
|
||||||
$item_ids = $_POST['item_ids'] ?? [];
|
$item_ids = $_POST['item_ids'] ?? [];
|
||||||
$quantities = $_POST['quantities'] ?? [];
|
$quantities = $_POST['quantities'] ?? [];
|
||||||
$prices = $_POST['prices'] ?? [];
|
$prices = $_POST['prices'] ?? [];
|
||||||
@ -799,8 +797,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$items_data[] = ['id' => $item_id, 'qty' => $qty, 'price' => $price, 'total' => $line_total];
|
$items_data[] = ['id' => $item_id, 'qty' => $qty, 'price' => $price, 'total' => $line_total];
|
||||||
}
|
}
|
||||||
$total_with_vat = $subtotal + $total_vat;
|
$total_with_vat = $subtotal + $total_vat;
|
||||||
$stmt = $db->prepare("UPDATE quotations SET customer_id = ?, quotation_date = ?, valid_until = ?, status = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, terms_conditions = ? WHERE id = ?");
|
$stmt = $db->prepare("UPDATE quotations SET customer_id = ?, quotation_date = ?, valid_until = ?, status = ?, total_amount = ?, vat_amount = ?, total_with_vat = ? WHERE id = ?");
|
||||||
$stmt->execute([$customer_id, $quotation_date, $valid_until, $status, $subtotal, $total_vat, $total_with_vat, $terms_conditions, $quotation_id]);
|
$stmt->execute([$customer_id, $quotation_date, $valid_until, $status, $subtotal, $total_vat, $total_with_vat, $quotation_id]);
|
||||||
foreach ($items_data as $item) {
|
foreach ($items_data as $item) {
|
||||||
$stmt = $db->prepare("INSERT INTO quotation_items (quotation_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
|
$stmt = $db->prepare("INSERT INTO quotation_items (quotation_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
|
||||||
$stmt->execute([$quotation_id, $item['id'], $item['qty'], $item['price'], $item['total']]);
|
$stmt->execute([$quotation_id, $item['id'], $item['qty'], $item['price'], $item['total']]);
|
||||||
@ -840,8 +838,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$items = $stmt->fetchAll();
|
$items = $stmt->fetchAll();
|
||||||
|
|
||||||
// Create Invoice
|
// Create Invoice
|
||||||
$stmt = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, type, status, total_amount, vat_amount, total_with_vat, paid_amount, terms_conditions) VALUES (?, CURDATE(), 'sale', 'unpaid', ?, ?, ?, 0, ?)");
|
$stmt = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, type, status, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, CURDATE(), 'sale', 'unpaid', ?, ?, ?, 0)");
|
||||||
$stmt->execute([$q['customer_id'], $q['total_amount'], $q['vat_amount'], $q['total_with_vat'], $q['terms_conditions']]);
|
$stmt->execute([$q['customer_id'], $q['total_amount'], $q['vat_amount'], $q['total_with_vat']]);
|
||||||
$invoice_id = $db->lastInsertId();
|
$invoice_id = $db->lastInsertId();
|
||||||
|
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
@ -1127,7 +1125,7 @@ switch ($page) {
|
|||||||
$params[] = $_GET['end_date'];
|
$params[] = $_GET['end_date'];
|
||||||
}
|
}
|
||||||
$whereSql = implode(" AND ", $where);
|
$whereSql = implode(" AND ", $where);
|
||||||
$stmt = db()->prepare("SELECT q.*, c.name as customer_name, c.phone as customer_phone, c.tax_id as customer_tax_id
|
$stmt = db()->prepare("SELECT q.*, c.name as customer_name
|
||||||
FROM quotations q
|
FROM quotations q
|
||||||
LEFT JOIN customers c ON q.customer_id = c.id
|
LEFT JOIN customers c ON q.customer_id = c.id
|
||||||
WHERE $whereSql
|
WHERE $whereSql
|
||||||
@ -2793,7 +2791,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
|||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
<button class="btn btn-outline-info view-quotation-btn" data-json="<?= htmlspecialchars(json_encode($q)) ?>" title="View"><i class="bi bi-eye"></i></button>
|
<button class="btn btn-outline-info view-quotation-btn" data-json="<?= htmlspecialchars(json_encode($q)) ?>" title="View"><i class="bi bi-eye"></i></button>
|
||||||
<button class="btn btn-outline-secondary print-quotation-btn" data-json="<?= htmlspecialchars(json_encode($q)) ?>" title="Print"><i class="bi bi-printer"></i></button>
|
<button class="btn btn-outline-secondary" onclick="window.viewAndPrintQuotation(<?= htmlspecialchars(json_encode($q)) ?>)" title="Print"><i class="bi bi-printer"></i></button>
|
||||||
<button class="btn btn-outline-primary edit-quotation-btn" data-json="<?= htmlspecialchars(json_encode($q)) ?>" data-bs-toggle="modal" data-bs-target="#editQuotationModal" title="Edit"><i class="bi bi-pencil-square"></i></button>
|
<button class="btn btn-outline-primary edit-quotation-btn" data-json="<?= htmlspecialchars(json_encode($q)) ?>" data-bs-toggle="modal" data-bs-target="#editQuotationModal" title="Edit"><i class="bi bi-pencil-square"></i></button>
|
||||||
<?php if ($q['status'] === 'pending'): ?>
|
<?php if ($q['status'] === 'pending'): ?>
|
||||||
<button class="btn btn-outline-success convert-quotation-btn" data-id="<?= $q['id'] ?>" title="Convert to Invoice"><i class="bi bi-receipt"></i></button>
|
<button class="btn btn-outline-success convert-quotation-btn" data-id="<?= $q['id'] ?>" title="Convert to Invoice"><i class="bi bi-receipt"></i></button>
|
||||||
@ -3443,175 +3441,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
// View Quotation Logic
|
|
||||||
window.viewAndPrintQuotation = function(data, autoPrint = false) {
|
|
||||||
if (!data || !data.items) {
|
|
||||||
console.error('Invalid quotation data');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const modalEl = document.getElementById('viewQuotationModal');
|
|
||||||
if (!modalEl) return;
|
|
||||||
|
|
||||||
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
|
||||||
const content = document.getElementById('quotationPrintableArea');
|
|
||||||
|
|
||||||
let itemsHtml = '';
|
|
||||||
data.items.forEach((item, index) => {
|
|
||||||
itemsHtml += `
|
|
||||||
<tr>
|
|
||||||
<td>${index + 1}</td>
|
|
||||||
<td>${item.name_en}<br><small>${item.name_ar}</small></td>
|
|
||||||
<td class="text-center">${item.quantity}</td>
|
|
||||||
<td class="text-end">${parseFloat(item.unit_price || 0).toFixed(3)}</td>
|
|
||||||
<td class="text-center">${item.vat_rate}%</td>
|
|
||||||
<td class="text-end">${parseFloat(item.total_price || 0).toFixed(3)}</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const logo = "<?= $data['settings']['company_logo'] ?? '' ?>";
|
|
||||||
const companyName = "<?= htmlspecialchars($data['settings']['company_name'] ?? '' ) ?>";
|
|
||||||
const companyPhone = "<?= htmlspecialchars($data['settings']['company_phone'] ?? '' ) ?>";
|
|
||||||
const companyEmail = "<?= htmlspecialchars($data['settings']['company_email'] ?? '' ) ?>";
|
|
||||||
const companyAddress = "<?= htmlspecialchars($data['settings']['company_address'] ?? '' ) ?>";
|
|
||||||
const vatNumber = "<?= htmlspecialchars($data['settings']['vat_number'] ?? '' ) ?>";
|
|
||||||
|
|
||||||
content.innerHTML = `
|
|
||||||
<div class="p-4">
|
|
||||||
<div class="d-flex justify-content-between align-items-start mb-4 border-bottom pb-4">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
${logo ? `<img src="${logo}" alt="Logo" style="max-height: 80px;" class="me-3">` : ''}
|
|
||||||
<div>
|
|
||||||
<h2 class="fw-bold mb-0 text-primary">${companyName}</h2>
|
|
||||||
<div class="small text-muted">
|
|
||||||
${companyAddress ? `<div>${companyAddress}</div>` : ''}
|
|
||||||
${companyPhone ? `<span>Tel: ${companyPhone}</span>` : ''}
|
|
||||||
${companyEmail ? `<span class="ms-3">Email: ${companyEmail}</span>` : ''}
|
|
||||||
${vatNumber ? `<div class="mt-1 fw-bold">VAT No: ${vatNumber}</div>` : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-end">
|
|
||||||
<h1 class="display-6 fw-bold text-uppercase mb-0" style="letter-spacing: 2px;">QUOTATION</h1>
|
|
||||||
<div class="mt-2 h5">
|
|
||||||
<span class="text-muted">No:</span> <span class="fw-bold">QUO-${(data.id || '').toString().padStart(5, '0')}</span>
|
|
||||||
</div>
|
|
||||||
<div class="small">
|
|
||||||
<span class="text-muted">Date:</span> <span>${data.quotation_date || ''}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-6">
|
|
||||||
<div class="bg-light p-3 rounded h-100">
|
|
||||||
<h6 class="text-muted text-uppercase small fw-bold mb-2">Quote To:</h6>
|
|
||||||
<h5 class="fw-bold mb-1">${data.customer_name || 'Walk-in Customer'}</h5>
|
|
||||||
${data.customer_phone ? `<div class="small">${data.customer_phone}</div>` : ''}
|
|
||||||
${data.customer_tax_id ? `<div class="small">VAT No: ${data.customer_tax_id}</div>` : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-6 text-end">
|
|
||||||
<div class="p-3 h-100">
|
|
||||||
<div class="mb-1"><span class="text-muted small text-uppercase">Status:</span> <span class="badge ${data.status === 'converted' ? 'bg-success' : 'bg-warning text-dark'} text-uppercase ms-2">${data.status || 'pending'}</span></div>
|
|
||||||
<div class="mb-1"><span class="text-muted small text-uppercase">Valid Until:</span> <span class="ms-2">${data.valid_until || '---'}</span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-bordered">
|
|
||||||
<thead class="bg-primary text-white text-uppercase small">
|
|
||||||
<tr>
|
|
||||||
<th class="text-center" style="width: 50px;">#</th>
|
|
||||||
<th>Item Description</th>
|
|
||||||
<th class="text-center" style="width: 100px;">Qty</th>
|
|
||||||
<th class="text-end" style="width: 120px;">Unit Price</th>
|
|
||||||
<th class="text-center" style="width: 80px;">VAT</th>
|
|
||||||
<th class="text-end" style="width: 120px;">Total</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>${itemsHtml}</tbody>
|
|
||||||
<tfoot class="bg-light">
|
|
||||||
<tr>
|
|
||||||
<th colspan="5" class="text-end py-2">Subtotal</th>
|
|
||||||
<td class="text-end py-2 fw-bold">${parseFloat(data.total_amount || 0).toFixed(3)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th colspan="5" class="text-end py-2">VAT Amount</th>
|
|
||||||
<td class="text-end py-2 fw-bold">${parseFloat(data.vat_amount || 0).toFixed(3)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="table-primary">
|
|
||||||
<th colspan="5" class="text-end py-2 h5 mb-0">Grand Total (OMR)</th>
|
|
||||||
<td class="text-end py-2 h5 mb-0 fw-bold">${parseFloat(data.total_with_vat || 0).toFixed(3)}</td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
|
||||||
<div class="row g-4">
|
|
||||||
<div class="col-8">
|
|
||||||
<h6 class="fw-bold text-uppercase small border-bottom pb-2 mb-2">Terms & Conditions</h6>
|
|
||||||
<div class="small text-muted" style="white-space: pre-line;">
|
|
||||||
${data.terms_conditions || `1. This quotation is valid for the period mentioned above.
|
|
||||||
2. Prices are inclusive of VAT as per Omani law.
|
|
||||||
3. Payment terms as agreed upon.`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-4 text-center">
|
|
||||||
<div class="mt-5 pt-4">
|
|
||||||
<div class="border-top border-dark pt-2 mx-auto" style="width: 80%;">
|
|
||||||
<div class="fw-bold">Authorized Signature</div>
|
|
||||||
<div class="small text-muted">${companyName}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const actionButtons = document.getElementById('quotationActionButtons');
|
|
||||||
if (actionButtons) {
|
|
||||||
actionButtons.innerHTML = '';
|
|
||||||
if (data.status === 'pending') {
|
|
||||||
const convertBtn = document.createElement('button');
|
|
||||||
convertBtn.className = 'btn btn-success me-2';
|
|
||||||
convertBtn.innerHTML = '<i class="bi bi-receipt"></i> Convert to Invoice';
|
|
||||||
convertBtn.onclick = function() {
|
|
||||||
if (confirm('Convert this quotation to an invoice?')) {
|
|
||||||
const f = document.createElement('form');
|
|
||||||
f.method = 'POST';
|
|
||||||
f.innerHTML = `<input type="hidden" name="convert_to_invoice" value="1"><input type="hidden" name="quotation_id" value="${data.id}">`;
|
|
||||||
document.body.appendChild(f);
|
|
||||||
f.submit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
actionButtons.appendChild(convertBtn);
|
|
||||||
|
|
||||||
const editBtn = document.createElement('button');
|
|
||||||
editBtn.className = 'btn btn-primary';
|
|
||||||
editBtn.innerHTML = '<i class="bi bi-pencil-square"></i> Edit';
|
|
||||||
editBtn.onclick = function() {
|
|
||||||
const editModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('editQuotationModal'));
|
|
||||||
modal.hide();
|
|
||||||
const originalEditBtn = document.querySelector(`.edit-quotation-btn[data-json*='"id":${data.id},']`) ||
|
|
||||||
document.querySelector(`.edit-quotation-btn[data-json*='"id":${data.id}']`);
|
|
||||||
if (originalEditBtn) originalEditBtn.click();
|
|
||||||
};
|
|
||||||
actionButtons.appendChild(editBtn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
modal.show();
|
|
||||||
if (autoPrint) {
|
|
||||||
setTimeout(() => { window.print(); }, 800);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const hasExpiryToggle = document.getElementById('hasExpiryToggle');
|
const hasExpiryToggle = document.getElementById('hasExpiryToggle');
|
||||||
const expiryDateContainer = document.getElementById('expiryDateContainer');
|
const expiryDateContainer = document.getElementById('expiryDateContainer');
|
||||||
@ -3652,6 +3482,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
togglePaidAmount('add_status', 'addPaidAmountContainer');
|
togglePaidAmount('add_status', 'addPaidAmountContainer');
|
||||||
togglePaidAmount('edit_status', 'editPaidAmountContainer');
|
togglePaidAmount('edit_status', 'editPaidAmountContainer');
|
||||||
|
|
||||||
|
// Pay Invoice Logic
|
||||||
|
document.querySelectorAll('.pay-invoice-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const id = this.getAttribute('data-id');
|
||||||
|
const total = parseFloat(this.getAttribute('data-total'));
|
||||||
|
const paid = parseFloat(this.getAttribute('data-paid') || 0);
|
||||||
|
const remaining = total - paid;
|
||||||
|
|
||||||
|
document.getElementById('pay_invoice_id').value = id;
|
||||||
|
document.getElementById('pay_invoice_total').value = `OMR ${total.toFixed(3)}`;
|
||||||
|
document.getElementById('pay_remaining_amount').value = `OMR ${remaining.toFixed(3)}`;
|
||||||
|
document.getElementById('pay_amount').value = remaining.toFixed(3);
|
||||||
|
document.getElementById('pay_amount').max = remaining.toFixed(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Show receipt modal if needed
|
// Show receipt modal if needed
|
||||||
<?php if (isset($_SESSION['trigger_receipt_modal'])):
|
<?php if (isset($_SESSION['trigger_receipt_modal'])):
|
||||||
$rid = (int)$_SESSION['show_receipt_id'];
|
$rid = (int)$_SESSION['show_receipt_id'];
|
||||||
@ -3781,78 +3627,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Global Functions for Invoice/Quotation Forms
|
// Invoice Form Logic
|
||||||
window.addItemToTable = function(item, tableBody, searchInput, suggestions, grandTotalEl, subtotalEl, totalVatEl, customData = null) {
|
const initInvoiceForm = (searchInputId, suggestionsId, tableBodyId, grandTotalId, subtotalId, totalVatId) => {
|
||||||
if (suggestions) suggestions.style.display = 'none';
|
|
||||||
if (searchInput) searchInput.value = '';
|
|
||||||
|
|
||||||
const existingRow = Array.from(tableBody.querySelectorAll('.item-id-input')).find(input => input.value == item.id);
|
|
||||||
if (existingRow && !customData) {
|
|
||||||
const row = existingRow.closest('tr');
|
|
||||||
const qtyInput = row.querySelector('.item-qty');
|
|
||||||
qtyInput.value = parseFloat(qtyInput.value) + 1;
|
|
||||||
window.recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
row.className = 'item-row';
|
|
||||||
const currentInvoiceType = window.invoiceType || 'sale';
|
|
||||||
const price = customData ? customData.unit_price : (currentInvoiceType === 'sale' ? item.sale_price : item.purchase_price);
|
|
||||||
const qty = customData ? customData.quantity : 1;
|
|
||||||
const vatRate = item.vat_rate || 0;
|
|
||||||
|
|
||||||
row.innerHTML = `
|
|
||||||
<td>
|
|
||||||
<input type="hidden" name="item_ids[]" class="item-id-input" value="${item.id}">
|
|
||||||
<input type="hidden" class="item-vat-rate" value="${vatRate}">
|
|
||||||
<div><strong>${item.name_en}</strong></div>
|
|
||||||
<div class="small text-muted">${item.name_ar} (${item.sku})</div>
|
|
||||||
</td>
|
|
||||||
<td><input type="number" step="0.001" name="quantities[]" class="form-control item-qty" value="${qty}" required></td>
|
|
||||||
<td><input type="number" step="0.001" name="prices[]" class="form-control item-price" value="${price}" required></td>
|
|
||||||
<td><input type="text" class="form-control bg-light" value="${vatRate}%" readonly></td>
|
|
||||||
<td><input type="number" step="0.001" class="form-control item-total" value="${(qty * price).toFixed(3)}" readonly></td>
|
|
||||||
<td><button type="button" class="btn btn-outline-danger btn-sm remove-row"><i class="bi bi-trash"></i></button></td>
|
|
||||||
`;
|
|
||||||
|
|
||||||
tableBody.appendChild(row);
|
|
||||||
window.attachRowListeners(row, tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
|
||||||
window.recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.recalculate = function(tableBody, grandTotalEl, subtotalEl, totalVatEl) {
|
|
||||||
let subtotal = 0;
|
|
||||||
let totalVat = 0;
|
|
||||||
tableBody.querySelectorAll('.item-row').forEach(row => {
|
|
||||||
const qty = parseFloat(row.querySelector('.item-qty').value) || 0;
|
|
||||||
const price = parseFloat(row.querySelector('.item-price').value) || 0;
|
|
||||||
const vatRate = parseFloat(row.querySelector('.item-vat-rate').value) || 0;
|
|
||||||
|
|
||||||
const total = qty * price;
|
|
||||||
const vatAmount = total * (vatRate / 100);
|
|
||||||
|
|
||||||
row.querySelector('.item-total').value = total.toFixed(3);
|
|
||||||
subtotal += total;
|
|
||||||
totalVat += vatAmount;
|
|
||||||
});
|
|
||||||
const grandTotal = subtotal + totalVat;
|
|
||||||
|
|
||||||
if (subtotalEl) subtotalEl.textContent = 'OMR ' + subtotal.toFixed(3);
|
|
||||||
if (totalVatEl) totalVatEl.textContent = 'OMR ' + totalVat.toFixed(3);
|
|
||||||
if (grandTotalEl) grandTotalEl.textContent = 'OMR ' + grandTotal.toFixed(3);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.attachRowListeners = function(row, tableBody, grandTotalEl, subtotalEl, totalVatEl) {
|
|
||||||
row.querySelector('.item-qty').addEventListener('input', () => window.recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl));
|
|
||||||
row.querySelector('.item-price').addEventListener('input', () => window.recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl));
|
|
||||||
row.querySelector('.remove-row').addEventListener('click', function() {
|
|
||||||
row.remove();
|
|
||||||
window.recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
window.initInvoiceForm = function(searchInputId, suggestionsId, tableBodyId, grandTotalId, subtotalId, totalVatId) {
|
|
||||||
const searchInput = document.getElementById(searchInputId);
|
const searchInput = document.getElementById(searchInputId);
|
||||||
const suggestions = document.getElementById(suggestionsId);
|
const suggestions = document.getElementById(suggestionsId);
|
||||||
const tableBody = document.getElementById(tableBodyId);
|
const tableBody = document.getElementById(tableBodyId);
|
||||||
@ -3887,7 +3663,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
<span class="text-muted small">Stock: ${item.stock_quantity}</span>
|
<span class="text-muted small">Stock: ${item.stock_quantity}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
btn.onclick = () => window.addItemToTable(item, tableBody, searchInput, suggestions, grandTotalEl, subtotalEl, totalVatEl);
|
btn.onclick = () => addItemToTable(item, tableBody, searchInput, suggestions, grandTotalEl, subtotalEl, totalVatEl);
|
||||||
suggestions.appendChild(btn);
|
suggestions.appendChild(btn);
|
||||||
});
|
});
|
||||||
suggestions.style.display = 'block';
|
suggestions.style.display = 'block';
|
||||||
@ -3898,6 +3674,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}, 300);
|
}, 300);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Close suggestions when clicking outside
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function(e) {
|
||||||
if (!searchInput.contains(e.target) && !suggestions.contains(e.target)) {
|
if (!searchInput.contains(e.target) && !suggestions.contains(e.target)) {
|
||||||
suggestions.style.display = 'none';
|
suggestions.style.display = 'none';
|
||||||
@ -3905,136 +3682,314 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
window.invoiceType = '<?= in_array($page, ["sales", "quotations"]) ? "sale" : ($page === "purchases" ? "purchase" : "") ?>';
|
function addItemToTable(item, tableBody, searchInput, suggestions, grandTotalEl, subtotalEl, totalVatEl, customData = null) {
|
||||||
|
if (suggestions) suggestions.style.display = 'none';
|
||||||
|
if (searchInput) searchInput.value = '';
|
||||||
|
|
||||||
|
const existingRow = Array.from(tableBody.querySelectorAll('.item-id-input')).find(input => input.value == item.id);
|
||||||
|
if (existingRow && !customData) {
|
||||||
|
const row = existingRow.closest('tr');
|
||||||
|
const qtyInput = row.querySelector('.item-qty');
|
||||||
|
qtyInput.value = parseFloat(qtyInput.value) + 1;
|
||||||
|
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.className = 'item-row';
|
||||||
|
const price = customData ? customData.unit_price : (invoiceType === 'sale' ? item.sale_price : item.purchase_price);
|
||||||
|
const qty = customData ? customData.quantity : 1;
|
||||||
|
const vatRate = item.vat_rate || 0;
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>
|
||||||
|
<input type="hidden" name="item_ids[]" class="item-id-input" value="${item.id}">
|
||||||
|
<input type="hidden" class="item-vat-rate" value="${vatRate}">
|
||||||
|
<div><strong>${item.name_en}</strong></div>
|
||||||
|
<div class="small text-muted">${item.name_ar} (${item.sku})</div>
|
||||||
|
</td>
|
||||||
|
<td><input type="number" step="0.001" name="quantities[]" class="form-control item-qty" value="${qty}" required></td>
|
||||||
|
<td><input type="number" step="0.001" name="prices[]" class="form-control item-price" value="${price}" required></td>
|
||||||
|
<td><input type="text" class="form-control bg-light" value="${vatRate}%" readonly></td>
|
||||||
|
<td><input type="number" step="0.001" class="form-control item-total" value="${(qty * price).toFixed(3)}" readonly></td>
|
||||||
|
<td><button type="button" class="btn btn-outline-danger btn-sm remove-row"><i class="bi bi-trash"></i></button></td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
attachRowListeners(row, tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
||||||
|
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl) {
|
||||||
|
let subtotal = 0;
|
||||||
|
let totalVat = 0;
|
||||||
|
tableBody.querySelectorAll('.item-row').forEach(row => {
|
||||||
|
const qty = parseFloat(row.querySelector('.item-qty').value) || 0;
|
||||||
|
const price = parseFloat(row.querySelector('.item-price').value) || 0;
|
||||||
|
const vatRate = parseFloat(row.querySelector('.item-vat-rate').value) || 0;
|
||||||
|
|
||||||
|
const total = qty * price;
|
||||||
|
const vatAmount = total * (vatRate / 100);
|
||||||
|
|
||||||
|
row.querySelector('.item-total').value = total.toFixed(3);
|
||||||
|
subtotal += total;
|
||||||
|
totalVat += vatAmount;
|
||||||
|
});
|
||||||
|
const grandTotal = subtotal + totalVat;
|
||||||
|
|
||||||
|
if (subtotalEl) subtotalEl.textContent = 'OMR ' + subtotal.toFixed(3);
|
||||||
|
if (totalVatEl) totalVatEl.textContent = 'OMR ' + totalVat.toFixed(3);
|
||||||
|
if (grandTotalEl) grandTotalEl.textContent = 'OMR ' + grandTotal.toFixed(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachRowListeners(row, tableBody, grandTotalEl, subtotalEl, totalVatEl) {
|
||||||
|
row.querySelector('.item-qty').addEventListener('input', () => recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl));
|
||||||
|
row.querySelector('.item-price').addEventListener('input', () => recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl));
|
||||||
|
row.querySelector('.remove-row').addEventListener('click', function() {
|
||||||
|
row.remove();
|
||||||
|
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const invoiceType = '<?= in_array($page, ["sales", "quotations"]) ? "sale" : ($page === "purchases" ? "purchase" : "") ?>';
|
||||||
initInvoiceForm('productSearchInput', 'searchSuggestions', 'invoiceItemsTableBody', 'grandTotal', 'subtotal', 'totalVat');
|
initInvoiceForm('productSearchInput', 'searchSuggestions', 'invoiceItemsTableBody', 'grandTotal', 'subtotal', 'totalVat');
|
||||||
initInvoiceForm('editProductSearchInput', 'editSearchSuggestions', 'editInvoiceItemsTableBody', 'edit_grandTotal', 'edit_subtotal', 'edit_totalVat');
|
initInvoiceForm('editProductSearchInput', 'editSearchSuggestions', 'editInvoiceItemsTableBody', 'edit_grandTotal', 'edit_subtotal', 'edit_totalVat');
|
||||||
|
|
||||||
// Quotation Form Logic
|
// Quotation Form Logic
|
||||||
window.initInvoiceForm('quotProductSearchInput', 'quotSearchSuggestions', 'quotItemsTableBody', 'quot_grand_display', 'quot_subtotal_display', 'quot_vat_display');
|
initInvoiceForm('quotProductSearchInput', 'quotSearchSuggestions', 'quotItemsTableBody', 'quot_grand_display', 'quot_subtotal_display', 'quot_vat_display');
|
||||||
window.initInvoiceForm('editQuotProductSearchInput', 'editQuotSearchSuggestions', 'editQuotItemsTableBody', 'edit_quot_grand_display', 'edit_quot_subtotal_display', 'edit_quot_vat_display');
|
initInvoiceForm('editQuotProductSearchInput', 'editQuotSearchSuggestions', 'editQuotItemsTableBody', 'edit_quot_grand_display', 'edit_quot_subtotal_display', 'edit_quot_vat_display');
|
||||||
|
|
||||||
// Global Actions Handler - Delegation for list buttons
|
document.querySelectorAll('.edit-quotation-btn').forEach(btn => {
|
||||||
document.addEventListener('click', function(e) {
|
btn.addEventListener('click', function() {
|
||||||
const target = e.target.closest('button');
|
const data = JSON.parse(this.dataset.json);
|
||||||
if (!target) return;
|
document.getElementById('edit_quotation_id').value = data.id;
|
||||||
|
document.getElementById('edit_quot_customer_id').value = data.customer_id;
|
||||||
|
document.getElementById('edit_quot_date').value = data.quotation_date;
|
||||||
|
document.getElementById('edit_quot_valid').value = data.valid_until || '';
|
||||||
|
document.getElementById('edit_quot_status').value = data.status || 'pending';
|
||||||
|
|
||||||
|
const tableBody = document.getElementById('editQuotItemsTableBody');
|
||||||
|
tableBody.innerHTML = '';
|
||||||
|
|
||||||
|
data.items.forEach(item => {
|
||||||
|
addItemToTable({
|
||||||
|
id: item.item_id,
|
||||||
|
name_en: item.name_en,
|
||||||
|
name_ar: item.name_ar,
|
||||||
|
sku: '',
|
||||||
|
vat_rate: item.vat_rate || 0
|
||||||
|
}, tableBody, null, null,
|
||||||
|
document.getElementById('edit_quot_grand_display'),
|
||||||
|
document.getElementById('edit_quot_subtotal_display'),
|
||||||
|
document.getElementById('edit_quot_vat_display'),
|
||||||
|
{ quantity: item.quantity, unit_price: item.unit_price });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// --- Quotations ---
|
document.querySelectorAll('.convert-quotation-btn').forEach(btn => {
|
||||||
if (target.classList.contains('view-quotation-btn')) {
|
btn.addEventListener('click', function() {
|
||||||
try {
|
if (confirm('Convert this quotation to an invoice? This will reduce stock.')) {
|
||||||
const data = JSON.parse(target.dataset.json);
|
|
||||||
if (window.viewAndPrintQuotation) window.viewAndPrintQuotation(data, false);
|
|
||||||
} catch(ex) { console.error(ex); }
|
|
||||||
}
|
|
||||||
else if (target.classList.contains('print-quotation-btn')) {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(target.dataset.json);
|
|
||||||
if (window.viewAndPrintQuotation) window.viewAndPrintQuotation(data, true);
|
|
||||||
} catch(ex) { console.error(ex); }
|
|
||||||
}
|
|
||||||
else if (target.classList.contains('edit-quotation-btn')) {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(target.dataset.json);
|
|
||||||
if (document.getElementById('edit_quotation_id')) {
|
|
||||||
document.getElementById('edit_quotation_id').value = data.id;
|
|
||||||
document.getElementById('edit_quot_customer_id').value = data.customer_id;
|
|
||||||
document.getElementById('edit_quot_date').value = data.quotation_date;
|
|
||||||
document.getElementById('edit_quot_valid').value = data.valid_until || '';
|
|
||||||
document.getElementById('edit_quot_status').value = data.status || 'pending';
|
|
||||||
document.getElementById('edit_quot_terms').value = data.terms_conditions || '';
|
|
||||||
|
|
||||||
const tableBody = document.getElementById('editQuotItemsTableBody');
|
|
||||||
if (tableBody) {
|
|
||||||
tableBody.innerHTML = '';
|
|
||||||
(data.items || []).forEach(item => {
|
|
||||||
if (window.addItemToTable) {
|
|
||||||
window.addItemToTable({
|
|
||||||
id: item.item_id, name_en: item.name_en, name_ar: item.name_ar,
|
|
||||||
sku: '', vat_rate: item.vat_rate || 0
|
|
||||||
}, tableBody, null, null,
|
|
||||||
document.getElementById('edit_quot_grand_display'),
|
|
||||||
document.getElementById('edit_quot_subtotal_display'),
|
|
||||||
document.getElementById('edit_quot_vat_display'),
|
|
||||||
{ quantity: item.quantity, unit_price: item.unit_price });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(ex) { console.error(ex); }
|
|
||||||
}
|
|
||||||
else if (target.classList.contains('convert-quotation-btn')) {
|
|
||||||
if (target.dataset.id && confirm('Convert this quotation to an invoice? This will reduce stock.')) {
|
|
||||||
const f = document.createElement('form');
|
const f = document.createElement('form');
|
||||||
f.method = 'POST';
|
f.method = 'POST';
|
||||||
f.innerHTML = `<input type="hidden" name="convert_to_invoice" value="1"><input type="hidden" name="quotation_id" value="${target.dataset.id}">`;
|
f.innerHTML = `<input type="hidden" name="convert_to_invoice" value="1"><input type="hidden" name="quotation_id" value="${this.dataset.id}">`;
|
||||||
document.body.appendChild(f);
|
document.body.appendChild(f);
|
||||||
f.submit();
|
f.submit();
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// --- Invoices ---
|
// View Quotation Logic
|
||||||
else if (target.classList.contains('view-invoice-btn')) {
|
window.viewAndPrintQuotation = function(data, autoPrint = false) {
|
||||||
try {
|
const modal = new bootstrap.Modal(document.getElementById('viewQuotationModal'));
|
||||||
const data = JSON.parse(target.dataset.json);
|
const content = document.getElementById('quotationPrintableArea');
|
||||||
if (window.viewAndPrintA4Invoice) window.viewAndPrintA4Invoice(data, false);
|
|
||||||
} catch(ex) { console.error(ex); }
|
let itemsHtml = '';
|
||||||
}
|
data.items.forEach((item, index) => {
|
||||||
else if (target.classList.contains('print-a4-btn')) {
|
itemsHtml += `
|
||||||
try {
|
<tr>
|
||||||
const data = JSON.parse(target.dataset.json);
|
<td>${index + 1}</td>
|
||||||
if (window.viewAndPrintA4Invoice) window.viewAndPrintA4Invoice(data, true);
|
<td>${item.name_en}<br><small>${item.name_ar}</small></td>
|
||||||
} catch(ex) { console.error(ex); }
|
<td class="text-center">${item.quantity}</td>
|
||||||
}
|
<td class="text-end">${parseFloat(item.unit_price).toFixed(3)}</td>
|
||||||
else if (target.classList.contains('edit-invoice-btn')) {
|
<td class="text-center">${item.vat_rate}%</td>
|
||||||
try {
|
<td class="text-end">${parseFloat(item.total_price).toFixed(3)}</td>
|
||||||
const data = JSON.parse(target.dataset.json);
|
</tr>
|
||||||
if (document.getElementById('edit_invoice_id')) {
|
`;
|
||||||
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;
|
content.innerHTML = `
|
||||||
document.getElementById('edit_payment_type').value = data.payment_type || 'cash';
|
<div class="p-5">
|
||||||
document.getElementById('edit_status').value = data.status || 'unpaid';
|
<div class="d-flex justify-content-between mb-4 border-bottom pb-3">
|
||||||
document.getElementById('edit_paid_amount').value = parseFloat(data.paid_amount || 0).toFixed(3);
|
<div>
|
||||||
|
<h3 class="text-primary fw-bold">QUOTATION</h3>
|
||||||
const paidContainer = document.getElementById('editPaidAmountContainer');
|
<p class="mb-0"><strong>No:</strong> QUO-${data.id.toString().padStart(5, '0')}</p>
|
||||||
if (paidContainer) paidContainer.style.display = (data.status === 'partially_paid') ? 'block' : 'none';
|
<p class="mb-0"><strong>Date:</strong> ${data.quotation_date}</p>
|
||||||
|
<p class="mb-0"><strong>Valid Until:</strong> ${data.valid_until || 'N/A'}</p>
|
||||||
const tableBody = document.getElementById('editInvoiceItemsTableBody');
|
</div>
|
||||||
if (tableBody) {
|
<div class="text-end">
|
||||||
tableBody.innerHTML = '';
|
<h4 class="fw-bold">${data.customer_name || 'Walk-in Customer'}</h4>
|
||||||
(data.items || []).forEach(item => {
|
<p class="mb-0">Status: <span class="badge ${data.status === 'converted' ? 'bg-success' : 'bg-secondary'}">${data.status.toUpperCase()}</span></p>
|
||||||
if (window.addItemToTable) {
|
</div>
|
||||||
window.addItemToTable({
|
</div>
|
||||||
id: item.item_id, name_en: item.name_en, name_ar: item.name_ar,
|
<table class="table table-bordered table-striped">
|
||||||
sku: '', vat_rate: item.vat_rate || 0
|
<thead class="bg-dark text-white">
|
||||||
}, tableBody, null, null,
|
<tr>
|
||||||
document.getElementById('edit_grandTotal'),
|
<th>#</th>
|
||||||
document.getElementById('edit_subtotal'),
|
<th>Item Description</th>
|
||||||
document.getElementById('edit_totalVat'),
|
<th class="text-center">Qty</th>
|
||||||
{ quantity: item.quantity, unit_price: item.unit_price });
|
<th class="text-end">Unit Price</th>
|
||||||
}
|
<th class="text-center">VAT</th>
|
||||||
});
|
<th class="text-end">Total</th>
|
||||||
}
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${itemsHtml}</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th colspan="5" class="text-end">Subtotal</th>
|
||||||
|
<td class="text-end fw-bold">${parseFloat(data.total_amount).toFixed(3)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th colspan="5" class="text-end">VAT Amount</th>
|
||||||
|
<td class="text-end fw-bold">${parseFloat(data.vat_amount).toFixed(3)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="table-primary">
|
||||||
|
<th colspan="5" class="text-end h5">Grand Total (OMR)</th>
|
||||||
|
<td class="text-end h5 fw-bold">${parseFloat(data.total_with_vat).toFixed(3)}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
<div class="mt-5 pt-3 border-top">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<p class="small text-muted">Terms & Conditions:</p>
|
||||||
|
<ul class="small text-muted">
|
||||||
|
<li>Quotation is valid until the date mentioned above.</li>
|
||||||
|
<li>Prices are inclusive of VAT where applicable.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end pt-4">
|
||||||
|
<div class="border-top d-inline-block px-5">Authorized Signature</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const actionButtons = document.getElementById('quotationActionButtons');
|
||||||
|
actionButtons.innerHTML = '';
|
||||||
|
if (data.status === 'pending') {
|
||||||
|
const convertBtn = document.createElement('button');
|
||||||
|
convertBtn.className = 'btn btn-success me-2';
|
||||||
|
convertBtn.innerHTML = '<i class="bi bi-receipt"></i> Convert to Invoice';
|
||||||
|
convertBtn.onclick = function() {
|
||||||
|
if (confirm('Convert this quotation to an invoice?')) {
|
||||||
|
const f = document.createElement('form');
|
||||||
|
f.method = 'POST';
|
||||||
|
f.innerHTML = `<input type="hidden" name="convert_to_invoice" value="1"><input type="hidden" name="quotation_id" value="${data.id}">`;
|
||||||
|
document.body.appendChild(f);
|
||||||
|
f.submit();
|
||||||
}
|
}
|
||||||
} catch(ex) { console.error(ex); }
|
};
|
||||||
|
actionButtons.appendChild(convertBtn);
|
||||||
|
|
||||||
|
const editBtn = document.createElement('button');
|
||||||
|
editBtn.className = 'btn btn-primary';
|
||||||
|
editBtn.innerHTML = '<i class="bi bi-pencil-square"></i> Edit';
|
||||||
|
editBtn.onclick = function() {
|
||||||
|
const editModal = new bootstrap.Modal(document.getElementById('editQuotationModal'));
|
||||||
|
modal.hide();
|
||||||
|
// Trigger the existing edit button click logic or manually populate
|
||||||
|
const originalEditBtn = document.querySelector(`.edit-quotation-btn[data-json*='"id":${data.id},']`) ||
|
||||||
|
document.querySelector(`.edit-quotation-btn[data-json*='"id":${data.id}']`);
|
||||||
|
if (originalEditBtn) originalEditBtn.click();
|
||||||
|
};
|
||||||
|
actionButtons.appendChild(editBtn);
|
||||||
}
|
}
|
||||||
else if (target.classList.contains('pay-invoice-btn')) {
|
|
||||||
const id = target.getAttribute('data-id');
|
modal.show();
|
||||||
const total = parseFloat(target.getAttribute('data-total'));
|
if (autoPrint) {
|
||||||
const paid = parseFloat(target.getAttribute('data-paid') || 0);
|
setTimeout(() => { window.print(); }, 500);
|
||||||
const remaining = total - paid;
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (document.getElementById('pay_invoice_id')) {
|
document.querySelectorAll('.view-quotation-btn').forEach(btn => {
|
||||||
document.getElementById('pay_invoice_id').value = id;
|
btn.addEventListener('click', function() {
|
||||||
document.getElementById('pay_invoice_total').value = `OMR ${total.toFixed(3)}`;
|
const data = JSON.parse(this.dataset.json);
|
||||||
document.getElementById('pay_remaining_amount').value = `OMR ${remaining.toFixed(3)}`;
|
window.viewAndPrintQuotation(data, false);
|
||||||
document.getElementById('pay_amount').value = remaining.toFixed(3);
|
});
|
||||||
document.getElementById('pay_amount').max = remaining.toFixed(3);
|
});
|
||||||
|
|
||||||
|
// 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_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
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||||
|
|
||||||
<!-- View Invoice Modal -->
|
<!-- View Invoice Modal -->
|
||||||
@ -4307,10 +4262,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3">
|
|
||||||
<label class="form-label fw-bold" data-en="Terms & Conditions" data-ar="الشروط والأحكام">Terms & Conditions</label>
|
|
||||||
<textarea name="terms_conditions" class="form-control" rows="3" placeholder="Enter terms and conditions here..."></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
||||||
@ -4400,10 +4351,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3">
|
|
||||||
<label class="form-label fw-bold" data-en="Terms & Conditions" data-ar="الشروط والأحكام">Terms & Conditions</label>
|
|
||||||
<textarea name="terms_conditions" id="edit_quot_terms" class="form-control" rows="3" placeholder="Enter terms and conditions here..."></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
||||||
@ -4441,10 +4388,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
body { background: white !important; margin: 0 !important; padding: 0 !important; }
|
body { background: white !important; margin: 0 !important; padding: 0 !important; }
|
||||||
.main-content { margin: 0 !important; padding: 0 !important; background: white !important; }
|
.main-content { margin: 0 !important; padding: 0 !important; background: white !important; }
|
||||||
.modal { position: absolute !important; left: 0 !important; top: 0 !important; margin: 0 !important; padding: 0 !important; overflow: visible !important; display: block !important; visibility: visible !important; background: white !important; }
|
.modal { position: absolute !important; left: 0 !important; top: 0 !important; margin: 0 !important; padding: 0 !important; overflow: visible !important; display: block !important; visibility: visible !important; background: white !important; }
|
||||||
#viewInvoiceModal, #viewQuotationModal { display: block !important; background: white !important; }
|
#viewInvoiceModal { display: block !important; background: white !important; }
|
||||||
#viewInvoiceModal .modal-dialog, #viewQuotationModal .modal-dialog { max-width: 100% !important; width: 100% !important; margin: 0 !important; padding: 0 !important; }
|
#viewInvoiceModal .modal-dialog { max-width: 100% !important; width: 100% !important; margin: 0 !important; padding: 0 !important; }
|
||||||
#viewInvoiceModal .modal-content, #viewQuotationModal .modal-content { border: none !important; box-shadow: none !important; background: white !important; }
|
#viewInvoiceModal .modal-content { border: none !important; box-shadow: none !important; background: white !important; }
|
||||||
#viewInvoiceModal .modal-body, #viewQuotationModal .modal-body { padding: 0 !important; margin: 0 !important; background: white !important; }
|
#viewInvoiceModal .modal-body { padding: 0 !important; margin: 0 !important; background: white !important; }
|
||||||
.table-bordered th, .table-bordered td { border: 1px solid #dee2e6 !important; }
|
.table-bordered th, .table-bordered td { border: 1px solid #dee2e6 !important; }
|
||||||
.bg-light { background-color: #f8f9fa !important; -webkit-print-color-adjust: exact; }
|
.bg-light { background-color: #f8f9fa !important; -webkit-print-color-adjust: exact; }
|
||||||
.text-primary { color: #0d6efd !important; -webkit-print-color-adjust: exact; }
|
.text-primary { color: #0d6efd !important; -webkit-print-color-adjust: exact; }
|
||||||
@ -4452,7 +4399,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
/* Ensure the modal is the only thing visible */
|
/* Ensure the modal is the only thing visible */
|
||||||
body > *:not(.main-content):not(.modal) { display: none !important; }
|
body > *:not(.main-content):not(.modal) { display: none !important; }
|
||||||
.main-content > *:not(#viewInvoiceModal):not(#viewQuotationModal):not(.modal) { display: none !important; }
|
.main-content > *:not(#viewInvoiceModal):not(.modal) { display: none !important; }
|
||||||
}
|
}
|
||||||
.invoice-logo { max-height: 80px; width: auto; }
|
.invoice-logo { max-height: 80px; width: auto; }
|
||||||
.invoice-header { border-bottom: 2px solid #333; padding-bottom: 20px; }
|
.invoice-header { border-bottom: 2px solid #333; padding-bottom: 20px; }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user