Autosave: 20260216-140209
This commit is contained in:
parent
aaef7ed36c
commit
d19d0d272e
701
index.php
701
index.php
@ -73,6 +73,37 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['action'])) {
|
||||
echo json_encode($payment);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_GET['action'] === 'get_held_carts') {
|
||||
header('Content-Type: application/json');
|
||||
$stmt = db()->query("SELECT h.*, c.name as customer_name FROM pos_held_carts h LEFT JOIN customers c ON h.customer_id = c.id ORDER BY h.id DESC");
|
||||
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_GET['action'] === 'validate_discount') {
|
||||
header('Content-Type: application/json');
|
||||
$code = $_GET['code'] ?? '';
|
||||
$stmt = db()->prepare("SELECT * FROM discount_codes WHERE code = ? AND status = 'active' AND (expiry_date IS NULL OR expiry_date >= CURDATE())");
|
||||
$stmt->execute([$code]);
|
||||
$discount = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($discount) {
|
||||
echo json_encode(['success' => true, 'discount' => $discount]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid or expired code']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_GET['action'] === 'get_customer_loyalty') {
|
||||
header('Content-Type: application/json');
|
||||
$id = (int)($_GET['customer_id'] ?? 0);
|
||||
$stmt = db()->prepare("SELECT loyalty_points FROM customers WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$points = $stmt->fetchColumn();
|
||||
echo json_encode(['success' => true, 'points' => (float)$points]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
@ -118,10 +149,23 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Loyalty Calculation: 1 point per 1 OMR spent on net amount
|
||||
$loyalty_earned = floor($net_amount);
|
||||
|
||||
// Check if credit is used for walk-in
|
||||
// Check if credit is used for walk-in or exceeds limit
|
||||
$credit_total = 0;
|
||||
foreach ($payments as $p) {
|
||||
if ($p['method'] === 'credit' && !$customer_id) {
|
||||
throw new Exception("Credit payment is only allowed for registered customers");
|
||||
if ($p['method'] === 'credit') {
|
||||
if (!$customer_id) {
|
||||
throw new Exception("Credit payment is only allowed for registered customers");
|
||||
}
|
||||
$credit_total += (float)$p['amount'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($customer_id && $credit_total > 0) {
|
||||
$stmt = $db->prepare("SELECT balance, credit_limit FROM customers WHERE id = ?");
|
||||
$stmt->execute([$customer_id]);
|
||||
$cust = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($cust['credit_limit'] > 0 && (abs($cust['balance'] - $credit_total) > $cust['credit_limit'])) {
|
||||
throw new Exception("Credit limit exceeded. Current Debt: " . number_format(abs($cust['balance']), 3) . ", Limit: " . number_format($cust['credit_limit'], 3));
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,13 +241,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['action']) && $_GET['action'] === 'get_held_carts') {
|
||||
header('Content-Type: application/json');
|
||||
$stmt = db()->query("SELECT h.*, c.name as customer_name FROM pos_held_carts h LEFT JOIN customers c ON h.customer_id = c.id ORDER BY h.id DESC");
|
||||
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_POST['action']) && $_POST['action'] === 'delete_held_cart') {
|
||||
header('Content-Type: application/json');
|
||||
$id = (int)$_POST['id'];
|
||||
@ -213,31 +250,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['action']) && $_GET['action'] === 'validate_discount') {
|
||||
header('Content-Type: application/json');
|
||||
$code = $_GET['code'] ?? '';
|
||||
$stmt = db()->prepare("SELECT * FROM discount_codes WHERE code = ? AND status = 'active' AND (expiry_date IS NULL OR expiry_date >= CURDATE())");
|
||||
$stmt->execute([$code]);
|
||||
$discount = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($discount) {
|
||||
echo json_encode(['success' => true, 'discount' => $discount]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid or expired code']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['action']) && $_GET['action'] === 'get_customer_loyalty') {
|
||||
header('Content-Type: application/json');
|
||||
$id = (int)($_GET['customer_id'] ?? 0);
|
||||
$stmt = db()->prepare("SELECT loyalty_points FROM customers WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$points = $stmt->fetchColumn();
|
||||
echo json_encode(['success' => true, 'points' => (float)$points]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_POST['edit_customer'])) {
|
||||
|
||||
$id = (int)$_POST['id'];
|
||||
$name = $_POST['name'] ?? '';
|
||||
$email = $_POST['email'] ?? '';
|
||||
@ -760,7 +774,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
$message = "Settings updated successfully!";
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['record_payment'])) {
|
||||
$invoice_id = (int)$_POST['invoice_id'];
|
||||
@ -807,6 +820,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Routing & Data Fetching
|
||||
@ -1644,10 +1658,14 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
<span class="input-group-text bg-transparent border-end-0"><i class="bi bi-search"></i></span>
|
||||
<input type="text" id="productSearch" class="form-control border-start-0" placeholder="Search products by name or SKU..." data-en="Search products..." data-ar="بحث عن منتجات...">
|
||||
</div>
|
||||
<div class="input-group" style="width: 250px;">
|
||||
<div class="input-group" style="width: 200px;">
|
||||
<span class="input-group-text bg-light border-end-0"><i class="bi bi-upc-scan"></i></span>
|
||||
<input type="text" id="barcodeInput" class="form-control border-start-0" placeholder="Scan barcode..." data-en="Scan barcode..." data-ar="امسح الباركود..." autofocus>
|
||||
</div>
|
||||
<button class="btn btn-warning d-flex align-items-center gap-2" onclick="cart.openHeldCartsModal()">
|
||||
<i class="bi bi-pause-btn-fill"></i>
|
||||
<span class="d-none d-xl-inline" data-en="Held List" data-ar="قائمة الانتظار">Held List</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="product-grid" id="productGrid">
|
||||
<?php foreach ($products as $p): ?>
|
||||
@ -1674,8 +1692,8 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
<div class="p-3 border-bottom d-flex justify-content-between align-items-center">
|
||||
<h6 class="m-0 fw-bold"><i class="bi bi-cart3 me-2"></i>Cart</h6>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-warning" onclick="cart.openHeldCartsModal()" title="Resume Cart"><i class="bi bi-pause-fill"></i></button>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="cart.hold()" title="Hold Cart"><i class="bi bi-hand-index-thumb"></i></button>
|
||||
<button class="btn btn-sm btn-outline-warning" onclick="cart.openHeldCartsModal()" title="Held List"><i class="bi bi-list-task"></i></button>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="cart.hold()" title="Hold Cart"><i class="bi bi-pause-circle"></i></button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="cart.clear()" title="Clear Cart"><i class="bi bi-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
@ -1836,42 +1854,77 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
}
|
||||
},
|
||||
async openHeldCartsModal() {
|
||||
const resp = await fetch('index.php?action=get_held_carts');
|
||||
const carts = await resp.json();
|
||||
let html = '<div class="list-group">';
|
||||
if (carts.length === 0) html += '<p class="text-center p-3">No held carts</p>';
|
||||
carts.forEach(c => {
|
||||
html += `
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div class="text-start">
|
||||
<strong>${c.cart_name}</strong><br>
|
||||
<small>${c.customer_name || 'Walk-in'} - ${c.created_at}</small>
|
||||
try {
|
||||
const resp = await fetch('index.php?action=get_held_carts');
|
||||
const text = await resp.text();
|
||||
let carts;
|
||||
try {
|
||||
carts = JSON.parse(text);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse held carts:', text);
|
||||
throw new Error('Invalid server response');
|
||||
}
|
||||
const lang = document.documentElement.lang || 'en';
|
||||
let html = '<div class="list-group list-group-flush shadow-sm rounded">';
|
||||
if (carts.length === 0) {
|
||||
html += `
|
||||
<div class="text-center p-5 text-muted">
|
||||
<i class="bi bi-folder2-open mb-3 d-block" style="font-size: 3rem;"></i>
|
||||
<p data-en="No held carts found" data-ar="لا توجد طلبات معلقة">${lang === 'ar' ? 'لا توجد طلبات معلقة' : 'No held carts found'}</p>
|
||||
</div>`;
|
||||
}
|
||||
carts.forEach(c => {
|
||||
html += `
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center p-3 hover-bg-light border-start-0 border-end-0">
|
||||
<div class="text-start">
|
||||
<div class="fw-bold text-primary">${c.cart_name}</div>
|
||||
<div class="small text-muted">
|
||||
<i class="bi bi-person me-1"></i>${c.customer_name || (lang === 'ar' ? 'عميل عابر' : 'Walk-in')}
|
||||
<span class="mx-2 text-silver">|</span>
|
||||
<i class="bi bi-clock me-1"></i>${new Date(c.created_at).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-primary" onclick="cart.resume(${c.id})">
|
||||
<i class="bi bi-arrow-repeat me-1"></i><span data-en="Resume" data-ar="استرجاع">${lang === 'ar' ? 'استرجاع' : 'Resume'}</span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="cart.deleteHeld(${c.id})">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-primary" onclick="cart.resume(${c.id})">Resume</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="cart.deleteHeld(${c.id})">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
Swal.fire({
|
||||
title: 'Held Carts',
|
||||
html: html,
|
||||
showConfirmButton: false,
|
||||
width: '600px'
|
||||
});
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
Swal.fire({
|
||||
title: lang === 'ar' ? 'الطلبات المعلقة' : 'Held Carts',
|
||||
html: html,
|
||||
showConfirmButton: false,
|
||||
width: '700px',
|
||||
customClass: {
|
||||
container: 'held-carts-swal'
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
Swal.fire('Error', 'Failed to load held carts: ' + err.message, 'error');
|
||||
}
|
||||
},
|
||||
async resume(id) {
|
||||
const resp = await fetch('index.php?action=get_held_carts');
|
||||
const carts = await resp.json();
|
||||
const c = carts.find(x => x.id == id);
|
||||
if (c) {
|
||||
this.items = JSON.parse(c.items_json);
|
||||
document.getElementById('posCustomer').value = c.customer_id || '';
|
||||
await this.onCustomerChange();
|
||||
await this.deleteHeld(id, true);
|
||||
Swal.close();
|
||||
try {
|
||||
const resp = await fetch('index.php?action=get_held_carts');
|
||||
const carts = await resp.json();
|
||||
const c = carts.find(x => x.id == id);
|
||||
if (c) {
|
||||
this.items = JSON.parse(c.items_json);
|
||||
document.getElementById('posCustomer').value = c.customer_id || '';
|
||||
await this.onCustomerChange();
|
||||
await this.deleteHeld(id, true);
|
||||
Swal.close();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
Swal.fire('Error', 'Failed to resume cart', 'error');
|
||||
}
|
||||
},
|
||||
async deleteHeld(id, silent = false) {
|
||||
@ -1942,6 +1995,10 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
async checkout() {
|
||||
if (this.items.length === 0) return;
|
||||
|
||||
const customerSelect = document.getElementById('posCustomer');
|
||||
const customerName = customerSelect.options[customerSelect.selectedIndex].text;
|
||||
document.getElementById('paymentCustomerName').innerText = customerName;
|
||||
|
||||
const subtotal = this.items.reduce((sum, item) => sum + (item.price * item.qty), 0);
|
||||
let discountAmount = 0;
|
||||
if (this.discount) {
|
||||
@ -1953,8 +2010,22 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
|
||||
this.payments = [];
|
||||
this.renderPayments();
|
||||
if (document.getElementById('creditCustomerSearch')) {
|
||||
document.getElementById('creditCustomerSearch').value = '';
|
||||
}
|
||||
document.getElementById('paymentAmountDue').innerText = 'OMR ' + total.toFixed(3);
|
||||
document.getElementById('partialAmount').value = total.toFixed(3);
|
||||
|
||||
// Sync credit customer selection if credit is default or already selected
|
||||
const creditSection = document.getElementById('creditCustomerSection');
|
||||
if (this.selectedPaymentMethod === 'credit') {
|
||||
creditSection.style.display = 'block';
|
||||
this.filterCreditCustomers();
|
||||
document.getElementById('paymentCreditCustomer').value = customerSelect.value;
|
||||
} else {
|
||||
creditSection.style.display = 'none';
|
||||
}
|
||||
|
||||
this.updateRemaining();
|
||||
|
||||
const modal = new bootstrap.Modal(document.getElementById('posPaymentModal'));
|
||||
@ -1964,6 +2035,20 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
this.selectedPaymentMethod = method;
|
||||
document.querySelectorAll('.payment-method-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
const creditSection = document.getElementById('creditCustomerSection');
|
||||
if (method === 'credit') {
|
||||
creditSection.style.display = 'block';
|
||||
if (document.getElementById('creditCustomerSearch')) {
|
||||
document.getElementById('creditCustomerSearch').value = '';
|
||||
}
|
||||
this.filterCreditCustomers();
|
||||
// Sync with main customer select
|
||||
document.getElementById('paymentCreditCustomer').value = document.getElementById('posCustomer').value;
|
||||
} else {
|
||||
creditSection.style.display = 'none';
|
||||
}
|
||||
|
||||
this.updateRemaining();
|
||||
},
|
||||
fillPartial(amount) {
|
||||
@ -2222,6 +2307,44 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
document.getElementById('posReceiptModal').addEventListener('hidden.bs.modal', function () {
|
||||
location.reload();
|
||||
}, { once: true });
|
||||
},
|
||||
filterCreditCustomers() {
|
||||
const searchInput = document.getElementById('creditCustomerSearch');
|
||||
if (!searchInput) return;
|
||||
const search = searchInput.value.toLowerCase();
|
||||
const select = document.getElementById('paymentCreditCustomer');
|
||||
if (!select) return;
|
||||
|
||||
if (!this.allCreditCustomers || (this.allCreditCustomers.length <= 1 && select.options.length > 1)) {
|
||||
this.allCreditCustomers = Array.from(select.options).map(opt => ({
|
||||
value: opt.value,
|
||||
text: opt.text,
|
||||
search: opt.getAttribute('data-search') || opt.text.toLowerCase()
|
||||
}));
|
||||
}
|
||||
|
||||
if (!this.allCreditCustomers) return;
|
||||
|
||||
const currentValue = select.value;
|
||||
select.innerHTML = '';
|
||||
|
||||
this.allCreditCustomers.forEach(opt => {
|
||||
if (opt.value === "" || opt.search.includes(search)) {
|
||||
const o = document.createElement('option');
|
||||
o.value = opt.value;
|
||||
o.text = opt.text;
|
||||
o.setAttribute('data-search', opt.search);
|
||||
if (opt.value === currentValue) o.selected = true;
|
||||
select.appendChild(o);
|
||||
}
|
||||
});
|
||||
},
|
||||
async syncCustomer(val) {
|
||||
document.getElementById('posCustomer').value = val;
|
||||
const customerSelect = document.getElementById('posCustomer');
|
||||
const customerName = customerSelect.options[customerSelect.selectedIndex].text;
|
||||
document.getElementById('paymentCustomerName').innerText = customerName;
|
||||
await this.onCustomerChange();
|
||||
}
|
||||
};
|
||||
|
||||
@ -2372,14 +2495,29 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
<td><?= $inv['invoice_date'] ?></td>
|
||||
<td><?= htmlspecialchars($inv['customer_name'] ?? '---') ?></td>
|
||||
<td>
|
||||
<span class="badge bg-secondary"><?= htmlspecialchars($inv['status']) ?></span>
|
||||
<?php
|
||||
$statusClass = 'bg-secondary';
|
||||
if ($inv['status'] === 'paid') $statusClass = 'bg-success';
|
||||
elseif ($inv['status'] === 'unpaid') $statusClass = 'bg-danger';
|
||||
elseif ($inv['status'] === 'partially_paid') $statusClass = 'bg-warning text-dark';
|
||||
?>
|
||||
<span class="badge text-uppercase <?= $statusClass ?>"><?= htmlspecialchars(str_replace('_', ' ', $inv['status'])) ?></span>
|
||||
</td>
|
||||
<td class="text-end fw-bold">OMR <?= number_format((float)$inv['total_with_vat'], 3) ?></td>
|
||||
<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['total_with_vat'] - $inv['paid_amount']), 3) ?></td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-info view-invoice-btn" data-json='<?= json_encode($inv) ?>' data-bs-toggle="modal" data-bs-target="#viewInvoiceModal"><i class="bi bi-printer"></i></button>
|
||||
<button class="btn btn-outline-info view-invoice-btn" data-json='<?= json_encode($inv) ?>' data-bs-toggle="modal" data-bs-target="#viewInvoiceModal" title="View"><i class="bi bi-eye"></i></button>
|
||||
<button class="btn btn-outline-primary edit-invoice-btn" data-json='<?= json_encode($inv) ?>' 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" onclick='viewAndPrintA4Invoice(<?= json_encode($inv) ?>)' 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>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -3379,6 +3517,206 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||
|
||||
<!-- View Invoice Modal -->
|
||||
<!-- Add Invoice Modal -->
|
||||
<div class="modal fade" id="addInvoiceModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title" data-en="Create New Tax Invoice" data-ar="إنشاء فاتورة ضريبية جديدة">Create New Tax Invoice</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="type" value="<?= $page === 'sales' ? 'sale' : 'purchase' ?>">
|
||||
<div class="modal-body p-4">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<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" class="form-select" required>
|
||||
<option value="">---</option>
|
||||
<?php foreach ($data['customers_list'] as $c): ?>
|
||||
<option value="<?= $c['id'] ?>"><?= htmlspecialchars($c['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label fw-bold" data-en="Date" data-ar="التاريخ">Date</label>
|
||||
<input type="date" name="invoice_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label fw-bold" data-en="Payment Type" data-ar="طريقة الدفع">Payment Type</label>
|
||||
<select name="payment_type" class="form-select">
|
||||
<option value="cash">Cash</option>
|
||||
<option value="card">Card</option>
|
||||
<option value="bank_transfer">Bank Transfer</option>
|
||||
<option value="credit">Credit</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label fw-bold" data-en="Status" data-ar="الحالة">Status</label>
|
||||
<select name="status" id="add_status" class="form-select">
|
||||
<option value="unpaid">Unpaid</option>
|
||||
<option value="partially_paid">Partially Paid</option>
|
||||
<option value="paid">Paid</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2" id="addPaidAmountContainer" style="display: none;">
|
||||
<label class="form-label fw-bold" data-en="Paid Amount" data-ar="المبلغ المدفوع">Paid Amount</label>
|
||||
<input type="number" step="0.001" name="paid_amount" class="form-control" value="0.000">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-body bg-light">
|
||||
<label class="form-label fw-bold" data-en="Search Items" data-ar="بحث عن أصناف">Search Items</label>
|
||||
<div class="position-relative">
|
||||
<input type="text" id="productSearchInput" class="form-control" placeholder="Search by name or SKU..." autocomplete="off">
|
||||
<div id="searchSuggestions" class="list-group position-absolute w-100 shadow-sm" style="display: none; z-index: 1000;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered align-middle">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th style="width: 40%;" data-en="Item Details" data-ar="تفاصيل الصنف">Item Details</th>
|
||||
<th style="width: 15%;" data-en="Qty" data-ar="الكمية">Qty</th>
|
||||
<th style="width: 15%;" data-en="Unit Price" data-ar="سعر الوحدة">Unit Price</th>
|
||||
<th style="width: 10%;" data-en="VAT" data-ar="الضريبة">VAT</th>
|
||||
<th style="width: 15%;" data-en="Total" data-ar="الإجمالي">Total</th>
|
||||
<th style="width: 5%;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="invoiceItemsTableBody"></tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="4" class="text-end fw-bold" data-en="Subtotal" data-ar="الإجمالي الفرعي">Subtotal</td>
|
||||
<td class="text-end fw-bold" id="subtotal">OMR 0.000</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="text-end fw-bold" data-en="Total VAT" data-ar="إجمالي الضريبة">Total VAT</td>
|
||||
<td class="text-end fw-bold" id="totalVat">OMR 0.000</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr class="table-primary">
|
||||
<td colspan="4" class="text-end fw-bold h5" data-en="Grand Total" data-ar="الإجمالي النهائي">Grand Total</td>
|
||||
<td class="text-end fw-bold h5" id="grandTotal">OMR 0.000</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
||||
<button type="submit" name="add_invoice" class="btn btn-primary" data-en="Create Invoice" data-ar="إنشاء الفاتورة">Create Invoice</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Invoice Modal -->
|
||||
<div class="modal fade" id="editInvoiceModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title" data-en="Edit Invoice" data-ar="تعديل الفاتورة">Edit Invoice</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="invoice_id" id="edit_invoice_id">
|
||||
<div class="modal-body p-4">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<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" required>
|
||||
<option value="">---</option>
|
||||
<?php foreach ($data['customers_list'] as $c): ?>
|
||||
<option value="<?= $c['id'] ?>"><?= htmlspecialchars($c['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label fw-bold" data-en="Date" data-ar="التاريخ">Date</label>
|
||||
<input type="date" name="invoice_date" id="edit_invoice_date" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label fw-bold" data-en="Payment Type" data-ar="طريقة الدفع">Payment Type</label>
|
||||
<select name="payment_type" id="edit_payment_type" class="form-select">
|
||||
<option value="cash">Cash</option>
|
||||
<option value="card">Card</option>
|
||||
<option value="bank_transfer">Bank Transfer</option>
|
||||
<option value="credit">Credit</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label fw-bold" data-en="Status" data-ar="الحالة">Status</label>
|
||||
<select name="status" id="edit_status" class="form-select">
|
||||
<option value="unpaid">Unpaid</option>
|
||||
<option value="partially_paid">Partially Paid</option>
|
||||
<option value="paid">Paid</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2" id="editPaidAmountContainer" style="display: none;">
|
||||
<label class="form-label fw-bold" data-en="Paid Amount" data-ar="المبلغ المدفوع">Paid Amount</label>
|
||||
<input type="number" step="0.001" name="paid_amount" id="edit_paid_amount" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-body bg-light">
|
||||
<label class="form-label fw-bold" data-en="Search Items" data-ar="بحث عن أصناف">Search Items</label>
|
||||
<div class="position-relative">
|
||||
<input type="text" id="editProductSearchInput" class="form-control" placeholder="Search by name or SKU..." autocomplete="off">
|
||||
<div id="editSearchSuggestions" class="list-group position-absolute w-100 shadow-sm" style="display: none; z-index: 1000;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered align-middle">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th style="width: 40%;" data-en="Item Details" data-ar="تفاصيل الصنف">Item Details</th>
|
||||
<th style="width: 15%;" data-en="Qty" data-ar="الكمية">Qty</th>
|
||||
<th style="width: 15%;" data-en="Unit Price" data-ar="سعر الوحدة">Unit Price</th>
|
||||
<th style="width: 10%;" data-en="VAT" data-ar="الضريبة">VAT</th>
|
||||
<th style="width: 15%;" data-en="Total" data-ar="الإجمالي">Total</th>
|
||||
<th style="width: 5%;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="editInvoiceItemsTableBody"></tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="4" class="text-end fw-bold" data-en="Subtotal" data-ar="الإجمالي الفرعي">Subtotal</td>
|
||||
<td class="text-end fw-bold" id="edit_subtotal">OMR 0.000</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="text-end fw-bold" data-en="Total VAT" data-ar="إجمالي الضريبة">Total VAT</td>
|
||||
<td class="text-end fw-bold" id="edit_totalVat">OMR 0.000</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr class="table-primary">
|
||||
<td colspan="4" class="text-end fw-bold h5" data-en="Grand Total" data-ar="الإجمالي النهائي">Grand Total</td>
|
||||
<td class="text-end fw-bold h5" id="edit_grandTotal">OMR 0.000</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
||||
<button type="submit" name="edit_invoice" class="btn btn-primary" data-en="Update Invoice" data-ar="تحديث الفاتورة">Update Invoice</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="viewInvoiceModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content border-0 shadow">
|
||||
@ -3474,6 +3812,55 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pay Invoice Modal -->
|
||||
<div class="modal fade" id="payInvoiceModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header bg-success text-white">
|
||||
<h5 class="modal-title" data-en="Record Payment" data-ar="تسجيل دفعة">Record Payment</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="invoice_id" id="pay_invoice_id">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold" data-en="Total Amount" data-ar="المبلغ الإجمالي">Total Amount</label>
|
||||
<input type="text" id="pay_invoice_total" class="form-control" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold" data-en="Remaining Amount" data-ar="المبلغ المتبقي">Remaining Amount</label>
|
||||
<input type="text" id="pay_remaining_amount" class="form-control" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold" data-en="Amount to Pay" data-ar="المبلغ المراد دفعه">Amount to Pay</label>
|
||||
<input type="number" step="0.001" name="amount" id="pay_amount" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold" data-en="Payment Date" data-ar="تاريخ الدفع">Payment Date</label>
|
||||
<input type="date" name="payment_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold" data-en="Payment Method" data-ar="طريقة الدفع">Payment Method</label>
|
||||
<select name="payment_method" class="form-select" required>
|
||||
<option value="Cash">Cash</option>
|
||||
<option value="Card">Card</option>
|
||||
<option value="Bank Transfer">Bank Transfer</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold" data-en="Notes" data-ar="ملاحظات">Notes</label>
|
||||
<textarea name="notes" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
||||
<button type="submit" name="record_payment" class="btn btn-success" data-en="Save Payment" data-ar="حفظ الدفعة">Save Payment</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment Receipt Modal -->
|
||||
<div class="modal fade" id="receiptModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
@ -3550,6 +3937,29 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3 p-3 border rounded bg-light shadow-sm">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="small text-muted d-block">Customer</span>
|
||||
<span class="h6 fw-bold m-0 text-primary" id="paymentCustomerName">Walk-in Customer</span>
|
||||
</div>
|
||||
<i class="bi bi-person-circle fs-3 text-secondary"></i>
|
||||
</div>
|
||||
<div id="creditCustomerSection" class="mt-2 pt-2 border-top" style="display:none;">
|
||||
<label class="form-label smaller fw-bold mb-1">Select Credit Customer (Search by Name/Phone)</label>
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<span class="input-group-text bg-white"><i class="bi bi-search"></i></span>
|
||||
<input type="text" id="creditCustomerSearch" class="form-control" placeholder="Search..." oninput="cart.filterCreditCustomers()">
|
||||
</div>
|
||||
<select id="paymentCreditCustomer" class="form-select form-select-sm" onchange="cart.syncCustomer(this.value)">
|
||||
<option value="">--- Select Customer ---</option>
|
||||
<?php foreach ($customers as $c): ?>
|
||||
<option value="<?= $c['id'] ?>" data-search="<?= htmlspecialchars(strtolower($c['name'] . ' ' . ($c['phone'] ?? ''))) ?>"><?= htmlspecialchars($c['name']) ?> (<?= htmlspecialchars($c['phone'] ?? '') ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="amount-due-box mb-3">
|
||||
<div class="d-flex justify-content-between px-3">
|
||||
<div class="text-start">
|
||||
@ -3650,6 +4060,151 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<div id="posPrintArea" class="d-none d-print-block"></div>
|
||||
|
||||
<script>
|
||||
window.viewAndPrintA4Invoice = function(data) {
|
||||
// Reuse view logic
|
||||
document.getElementById('invNumber').textContent = 'INV-' + data.id.toString().padStart(5, '0');
|
||||
document.getElementById('invDate').textContent = data.invoice_date;
|
||||
document.getElementById('invPaymentType').textContent = data.payment_type ? data.payment_type.toUpperCase() : 'CASH';
|
||||
document.getElementById('invCustomerName').textContent = data.customer_name || '---';
|
||||
|
||||
const taxIdEl = document.getElementById('invCustomerTaxId');
|
||||
const taxIdContainer = document.getElementById('invCustomerTaxIdContainer');
|
||||
if (data.customer_tax_id) {
|
||||
taxIdEl.textContent = data.customer_tax_id;
|
||||
taxIdContainer.style.display = 'block';
|
||||
} else {
|
||||
taxIdContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('invPartyLabel').textContent = data.type === 'sale' ? 'Bill To' : 'Bill From';
|
||||
document.getElementById('invPartyLabel').setAttribute('data-en', data.type === 'sale' ? 'Bill To' : 'Bill From');
|
||||
document.getElementById('invPartyLabel').setAttribute('data-ar', data.type === 'sale' ? 'فاتورة إلى' : 'فاتورة من');
|
||||
document.getElementById('invoiceTypeLabel').textContent = data.type;
|
||||
document.getElementById('invoiceTypeLabel').className = 'badge text-uppercase ' + (data.type === 'sale' ? 'bg-success' : 'bg-warning');
|
||||
|
||||
const statusLabel = document.getElementById('invoiceStatusLabel');
|
||||
let statusClass = 'bg-secondary';
|
||||
let statusEn = data.status.charAt(0).toUpperCase() + data.status.slice(1);
|
||||
if (data.status === 'paid') statusClass = 'bg-success';
|
||||
else if (data.status === 'unpaid') statusClass = 'bg-danger';
|
||||
else if (data.status === 'partially_paid') {
|
||||
statusClass = 'bg-warning text-dark';
|
||||
statusEn = 'Partially Paid';
|
||||
}
|
||||
|
||||
statusLabel.textContent = statusEn;
|
||||
statusLabel.className = 'badge text-uppercase ' + statusClass;
|
||||
|
||||
const body = document.getElementById('invItemsBody');
|
||||
body.innerHTML = '';
|
||||
data.items.forEach(item => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${item.name_en} / ${item.name_ar}</td>
|
||||
<td class="text-center">${item.quantity}</td>
|
||||
<td class="text-end">OMR ${parseFloat(item.unit_price).toFixed(3)}</td>
|
||||
<td class="text-end">${parseFloat(item.vat_rate || 0).toFixed(3)}%</td>
|
||||
<td class="text-end">OMR ${parseFloat(item.total_price).toFixed(3)}</td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
});
|
||||
document.getElementById('invSubtotal').textContent = 'OMR ' + parseFloat(data.total_amount).toFixed(3);
|
||||
document.getElementById('invVatAmount').textContent = 'OMR ' + (parseFloat(data.vat_amount) || 0).toFixed(3);
|
||||
const grandTotalValue = (parseFloat(data.total_with_vat) || parseFloat(data.total_amount));
|
||||
document.getElementById('invGrandTotal').textContent = 'OMR ' + grandTotalValue.toFixed(3);
|
||||
|
||||
document.getElementById('invTotalInfo').textContent = 'OMR ' + grandTotalValue.toFixed(3);
|
||||
document.getElementById('invPaidInfo').textContent = 'OMR ' + parseFloat(data.paid_amount || 0).toFixed(3);
|
||||
const balance = grandTotalValue - parseFloat(data.paid_amount || 0);
|
||||
document.getElementById('invBalanceInfo').textContent = 'OMR ' + balance.toFixed(3);
|
||||
|
||||
fetch(`index.php?action=get_payments&invoice_id=${data.id}`)
|
||||
.then(res => res.json())
|
||||
.then(payments => {
|
||||
const paymentsBody = document.getElementById('invPaymentsBody');
|
||||
const paymentsSection = document.getElementById('invPaymentsSection');
|
||||
paymentsBody.innerHTML = '';
|
||||
if (payments && payments.length > 0) {
|
||||
payments.forEach(p => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${p.payment_date}</td><td>${p.payment_method}</td><td class="text-end fw-bold">OMR ${parseFloat(p.amount).toFixed(3)}</td>`;
|
||||
paymentsBody.appendChild(tr);
|
||||
});
|
||||
paymentsSection.style.display = 'block';
|
||||
} else {
|
||||
paymentsSection.style.display = 'none';
|
||||
}
|
||||
|
||||
const viewModal = new bootstrap.Modal(document.getElementById('viewInvoiceModal'));
|
||||
viewModal.show();
|
||||
setTimeout(() => { window.print(); }, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
window.printPosReceiptFromInvoice = function(inv) {
|
||||
const container = document.getElementById('posReceiptContent');
|
||||
const itemsHtml = inv.items.map(item => `
|
||||
<tr>
|
||||
<td>${item.name_en}<br><small>${item.quantity} x ${parseFloat(item.unit_price).toFixed(3)}</small></td>
|
||||
<td style="text-align: right; vertical-align: bottom;">${(item.unit_price * item.quantity).toFixed(3)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
const companyName = "<?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>";
|
||||
const companyPhone = "<?= htmlspecialchars($data['settings']['company_phone'] ?? '') ?>";
|
||||
const companyVat = "<?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?>";
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="thermal-receipt">
|
||||
<div class="center">
|
||||
<h5 class="mb-0 fw-bold">${companyName}</h5>
|
||||
${companyPhone ? `<div>Tel: ${companyPhone}</div>` : ''}
|
||||
${companyVat ? `<div>VAT: ${companyVat}</div>` : ''}
|
||||
<div class="separator"></div>
|
||||
<h6 class="fw-bold">TAX INVOICE</h6>
|
||||
<div>Inv: INV-${inv.id.toString().padStart(5, '0')}</div>
|
||||
<div>Date: ${inv.invoice_date}</div>
|
||||
<div class="separator"></div>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Customer:</strong> ${inv.customer_name || 'Walk-in'}
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ITEM</th>
|
||||
<th style="text-align: right;">TOTAL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${itemsHtml}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="separator"></div>
|
||||
<div class="total-row d-flex justify-content-between">
|
||||
<span>TOTAL</span>
|
||||
<span>OMR ${parseFloat(inv.total_with_vat).toFixed(3)}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small">
|
||||
<span>PAID</span>
|
||||
<span>OMR ${parseFloat(inv.paid_amount).toFixed(3)}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small fw-bold">
|
||||
<span>BALANCE</span>
|
||||
<span>OMR ${(inv.total_with_vat - inv.paid_amount).toFixed(3)}</span>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div class="center small">
|
||||
<p>Thank You for your business!</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const posModal = new bootstrap.Modal(document.getElementById('posReceiptModal'));
|
||||
posModal.show();
|
||||
};
|
||||
|
||||
function printPosReceipt() {
|
||||
const content = document.getElementById('posReceiptContent').innerHTML;
|
||||
const printArea = document.getElementById('posPrintArea');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user