diff --git a/assets/css/custom.css b/assets/css/custom.css
index 9313a49..113a9e1 100644
--- a/assets/css/custom.css
+++ b/assets/css/custom.css
@@ -65,7 +65,7 @@ body {
}
.nav-section-title {
- font-size: 0.7rem;
+ font-size: 0.85rem;
font-weight: 600;
color: #64748b !important;
letter-spacing: 0.05em;
@@ -303,3 +303,90 @@ body {
top: 0;
}
}
+
+/* Payment Modal Styles */
+.payment-methods-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 10px;
+ margin-bottom: 20px;
+}
+.payment-method-btn {
+ border: 2px solid #e2e8f0;
+ background: #fff;
+ padding: 15px 10px;
+ border-radius: 10px;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+.payment-method-btn.active {
+ border-color: #3b82f6;
+ background: #eff6ff;
+ color: #3b82f6;
+}
+.payment-method-btn i {
+ font-size: 1.5rem;
+ display: block;
+ margin-bottom: 5px;
+}
+
+.quick-pay-grid {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ gap: 5px;
+ margin-top: 10px;
+}
+.quick-pay-btn {
+ padding: 10px;
+ border: 1px solid #e2e8f0;
+ background: #f8fafc;
+ border-radius: 6px;
+ text-align: center;
+ cursor: pointer;
+ font-weight: 600;
+}
+.quick-pay-btn:hover {
+ background: #edf2f7;
+}
+
+.amount-due-box {
+ background: #f1f5f9;
+ padding: 15px;
+ border-radius: 10px;
+ text-align: center;
+ margin-bottom: 20px;
+}
+.amount-due-box .label {
+ font-size: 0.75rem;
+ color: #64748b;
+ text-transform: uppercase;
+ font-weight: 600;
+}
+.amount-due-box .value {
+ font-size: 2.5rem;
+ font-weight: 800;
+ color: #0f172a;
+}
+.payment-line {
+ background: #f8f9fa;
+ border-radius: 8px;
+ padding: 10px;
+ margin-bottom: 10px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border: 1px solid #e9ecef;
+}
+.payment-line .method {
+ font-weight: 600;
+ text-transform: capitalize;
+}
+.payment-line .amount {
+ font-weight: 700;
+}
+.payment-summary {
+ border-top: 2px dashed #dee2e6;
+ margin-top: 15px;
+ padding-top: 15px;
+}
diff --git a/index.php b/index.php
index 38f2814..a99e672 100644
--- a/index.php
+++ b/index.php
@@ -101,19 +101,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$db->beginTransaction();
$customer_id = !empty($_POST['customer_id']) ? (int)$_POST['customer_id'] : null;
- $payment_method = $_POST['payment_method'] ?? 'cash';
+ $payments = json_decode($_POST['payments'] ?? '[]', true);
$total_amount = (float)$_POST['total_amount'];
$discount_code_id = !empty($_POST['discount_code_id']) ? (int)$_POST['discount_code_id'] : null;
$discount_amount = (float)($_POST['discount_amount'] ?? 0);
$loyalty_redeemed = (float)($_POST['loyalty_redeemed'] ?? 0);
- $items = json_decode($_POST['items'], true);
+ $items = json_decode($_POST['items'] ?? '[]', true);
- $net_amount = $total_amount - $discount_amount - $loyalty_redeemed;
+ if (empty($items)) {
+ throw new Exception("Cart is empty");
+ }
+
+ $net_amount = (float)($total_amount - $discount_amount - $loyalty_redeemed);
if ($net_amount < 0) $net_amount = 0;
// Loyalty Calculation: 1 point per 1 OMR spent on net amount
$loyalty_earned = floor($net_amount);
+ // Check if credit is used for walk-in
+ foreach ($payments as $p) {
+ if ($p['method'] === 'credit' && !$customer_id) {
+ throw new Exception("Credit payment is only allowed for registered customers");
+ }
+ }
+
// Create Invoice
$stmt = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, status, total_with_vat, paid_amount, type) VALUES (?, CURDATE(), 'paid', ?, ?, 'sale')");
$stmt->execute([$customer_id, $net_amount, $net_amount]);
@@ -122,37 +133,52 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Add POS Transaction record
$stmt = $db->prepare("INSERT INTO pos_transactions (transaction_no, customer_id, total_amount, discount_code_id, discount_amount, loyalty_points_earned, loyalty_points_redeemed, net_amount, payment_method) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$transaction_no = 'POS-' . time() . rand(100, 999);
- $stmt->execute([$transaction_no, $customer_id, $total_amount, $discount_code_id, $discount_amount, $loyalty_earned, $loyalty_redeemed, $net_amount, $payment_method]);
+ $methods_str = implode(', ', array_unique(array_map(fn($p) => $p['method'], $payments)));
+ $stmt->execute([$transaction_no, $customer_id, $total_amount, $discount_code_id, $discount_amount, $loyalty_earned, $loyalty_redeemed, $net_amount, $methods_str]);
$pos_id = $db->lastInsertId();
foreach ($items as $item) {
+ $qty = (float)$item['qty'];
+ $price = (float)$item['price'];
+ $subtotal = $qty * $price;
+
// Add to invoice_items
$stmt = $db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
- $stmt->execute([$invoice_id, $item['id'], $item['qty'], $item['price'], $item['qty'] * $item['price']]);
+ $stmt->execute([$invoice_id, $item['id'], $qty, $price, $subtotal]);
// Add to pos_items
$stmt = $db->prepare("INSERT INTO pos_items (transaction_id, product_id, quantity, unit_price, subtotal) VALUES (?, ?, ?, ?, ?)");
- $stmt->execute([$pos_id, $item['id'], $item['qty'], $item['price'], $item['qty'] * $item['price']]);
+ $stmt->execute([$pos_id, $item['id'], $qty, $price, $subtotal]);
// Update stock
$stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
- $stmt->execute([$item['qty'], $item['id']]);
+ $stmt->execute([$qty, $item['id']]);
}
- // Update Customer Loyalty Points
+ // Update Customer Loyalty Points and Balance
if ($customer_id) {
- $stmt = $db->prepare("UPDATE customers SET loyalty_points = loyalty_points - ? + ? WHERE id = ?");
- $stmt->execute([$loyalty_redeemed, $loyalty_earned, $customer_id]);
+ $credit_total = 0;
+ foreach ($payments as $p) {
+ if ($p['method'] === 'credit') {
+ $credit_total += (float)$p['amount'];
+ }
+ }
+
+ $stmt = $db->prepare("UPDATE customers SET loyalty_points = loyalty_points - ? + ?, balance = balance - ? WHERE id = ?");
+ $stmt->execute([(float)$loyalty_redeemed, (float)$loyalty_earned, (float)$credit_total, $customer_id]);
}
- // Add Payment
- $stmt = $db->prepare("INSERT INTO payments (invoice_id, amount, payment_date, payment_method, notes) VALUES (?, ?, CURDATE(), ?, 'POS Transaction')");
- $stmt->execute([$invoice_id, $net_amount, $payment_method]);
+ // Add Payments
+ foreach ($payments as $p) {
+ $stmt = $db->prepare("INSERT INTO payments (invoice_id, amount, payment_date, payment_method, notes) VALUES (?, ?, CURDATE(), ?, 'POS Transaction')");
+ $stmt->execute([$invoice_id, (float)$p['amount'], $p['method']]);
+ }
$db->commit();
echo json_encode(['success' => true, 'invoice_id' => $invoice_id, 'transaction_no' => $transaction_no]);
} catch (Exception $e) {
- $db->rollBack();
+ if (isset($db)) $db->rollBack();
+ error_log("POS Error: " . $e->getMessage());
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
@@ -1070,7 +1096,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
-
+
Operations
@@ -1090,7 +1116,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
-
+
Inventory
@@ -1107,7 +1133,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
-
+
Relationships
@@ -1121,7 +1147,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
-
+
Reports
@@ -1135,7 +1161,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
-
+
Configuration
@@ -1674,14 +1700,6 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
-
-
-
-
@@ -1721,6 +1739,8 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
items: [],
discount: null,
customerPoints: 0,
+ selectedPaymentMethod: 'cash',
+ payments: [],
add(product) {
const existing = this.items.find(item => item.id === product.id);
if (existing) {
@@ -1922,23 +1942,170 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
async checkout() {
if (this.items.length === 0) return;
- const btn = document.getElementById('checkoutBtn');
+ const subtotal = this.items.reduce((sum, item) => sum + (item.price * item.qty), 0);
+ let discountAmount = 0;
+ if (this.discount) {
+ discountAmount = this.discount.type === 'percentage' ? subtotal * (parseFloat(this.discount.value) / 100) : parseFloat(this.discount.value);
+ }
+ const redeemSwitch = document.getElementById('redeemLoyalty');
+ let loyaltyRedeemed = (redeemSwitch && redeemSwitch.checked) ? Math.min(subtotal - discountAmount, this.customerPoints) : 0;
+ const total = subtotal - discountAmount - loyaltyRedeemed;
+
+ this.payments = [];
+ this.renderPayments();
+ document.getElementById('paymentAmountDue').innerText = 'OMR ' + total.toFixed(3);
+ document.getElementById('partialAmount').value = total.toFixed(3);
+ this.updateRemaining();
+
+ const modal = new bootstrap.Modal(document.getElementById('posPaymentModal'));
+ modal.show();
+ },
+ selectMethod(method, btn) {
+ this.selectedPaymentMethod = method;
+ document.querySelectorAll('.payment-method-btn').forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+ this.updateRemaining();
+ },
+ fillPartial(amount) {
+ const input = document.getElementById('partialAmount');
+ input.value = parseFloat(amount).toFixed(3);
+ this.updateRemaining();
+ },
+ addPaymentLine() {
+ const amount = parseFloat(document.getElementById('partialAmount').value) || 0;
+ if (amount <= 0) return;
+
+ this.payments.push({
+ method: this.selectedPaymentMethod,
+ amount: amount
+ });
+ this.renderPayments();
+ this.updateRemaining();
+
+ // Auto-fill remaining for next line if any
+ const remaining = this.getRemaining();
+ document.getElementById('partialAmount').value = remaining > 0 ? remaining.toFixed(3) : '0.000';
+ },
+ removePaymentLine(index) {
+ this.payments.splice(index, 1);
+ this.renderPayments();
+ this.updateRemaining();
+ },
+ getGrandTotal() {
+ const subtotal = this.items.reduce((sum, item) => sum + (item.price * item.qty), 0);
+ let discountAmount = 0;
+ if (this.discount) {
+ discountAmount = this.discount.type === 'percentage' ? subtotal * (parseFloat(this.discount.value) / 100) : parseFloat(this.discount.value);
+ }
+ const redeemSwitch = document.getElementById('redeemLoyalty');
+ let loyaltyRedeemed = (redeemSwitch && redeemSwitch.checked) ? Math.min(subtotal - discountAmount, this.customerPoints) : 0;
+ return subtotal - discountAmount - loyaltyRedeemed;
+ },
+ getRemaining() {
+ const total = this.getGrandTotal();
+ const paid = this.payments.reduce((sum, p) => sum + p.amount, 0);
+ return total - paid;
+ },
+ renderPayments() {
+ const container = document.getElementById('paymentList');
+ container.innerHTML = this.payments.map((p, i) => `
+
+
+ ${p.method}
+ OMR ${p.amount.toFixed(3)}
+
+
+
+ `).join('');
+ },
+ updateRemaining() {
+ const remaining = this.getRemaining();
+ const currentInput = parseFloat(document.getElementById('partialAmount').value) || 0;
+ const display = document.getElementById('paymentRemaining');
+ display.innerText = 'OMR ' + Math.max(0, remaining).toFixed(3);
+
+ // Calculate potential change if the user types an amount > remaining
+ const totalPaid = this.payments.reduce((sum, p) => sum + p.amount, 0);
+ const grandTotal = this.getGrandTotal();
+ const actualChange = Math.max(0, totalPaid - grandTotal);
+ const potentialChange = Math.max(0, currentInput - remaining);
+ const displayChange = Math.max(actualChange, potentialChange);
+
+ const changeDisplay = document.getElementById('changeDue');
+ if (changeDisplay) {
+ changeDisplay.innerText = 'OMR ' + displayChange.toFixed(3);
+ const cashSection = document.getElementById('cashPaymentSection');
+ if (displayChange > 0 || this.selectedPaymentMethod === 'cash' || this.payments.some(p => p.method === 'cash')) {
+ cashSection.style.display = 'block';
+ } else {
+ cashSection.style.display = 'none';
+ }
+ }
+
+ if (remaining <= 0.0001 || currentInput >= remaining - 0.0001) {
+ display.classList.remove('text-danger');
+ display.classList.add('text-success');
+ document.getElementById('confirmPaymentBtn').disabled = false;
+ } else {
+ display.classList.remove('text-success');
+ display.classList.add('text-danger');
+ document.getElementById('confirmPaymentBtn').disabled = true;
+ }
+ },
+ async completeOrder() {
+ if (this.items.length === 0) {
+ Swal.fire('Error', 'Cart is empty', 'error');
+ return;
+ }
+
+ // If there's an amount in the input and payments are not enough, add it
+ const remainingBefore = this.getRemaining();
+ const currentInput = parseFloat(document.getElementById('partialAmount').value) || 0;
+
+ if (remainingBefore > 0.0001 && currentInput >= remainingBefore - 0.0001) {
+ this.payments.push({
+ method: this.selectedPaymentMethod,
+ amount: currentInput
+ });
+ } else if (this.payments.length === 0) {
+ const total = this.getGrandTotal();
+ this.payments.push({
+ method: this.selectedPaymentMethod,
+ amount: total
+ });
+ }
+
+ const remaining = this.getRemaining();
+ if (remaining > 0.001) {
+ Swal.fire('Error', 'Payment is incomplete', 'error');
+ return;
+ }
+
+ const customerId = document.getElementById('posCustomer').value;
+ if (this.payments.some(p => p.method === 'credit') && !customerId) {
+ Swal.fire('Error', 'Credit payment is only allowed for registered customers', 'error');
+ return;
+ }
+
+ const btn = document.getElementById('confirmPaymentBtn');
const originalText = btn.innerText;
btn.disabled = true;
btn.innerText = 'PROCESSING...';
- const subtotal = this.items.reduce((sum, item) => sum + (item.price * item.qty), 0);
+ const subtotal = this.items.reduce((sum, item) => sum + (parseFloat(item.price) * item.qty), 0);
let discountAmount = 0;
if (this.discount) {
- discountAmount = this.discount.type === 'percentage' ? subtotal * (this.discount.value / 100) : this.discount.value;
+ discountAmount = this.discount.type === 'percentage' ? subtotal * (parseFloat(this.discount.value) / 100) : parseFloat(this.discount.value);
}
const redeemSwitch = document.getElementById('redeemLoyalty');
let loyaltyRedeemed = (redeemSwitch && redeemSwitch.checked) ? Math.min(subtotal - discountAmount, this.customerPoints) : 0;
const formData = new FormData();
formData.append('action', 'save_pos_transaction');
- formData.append('customer_id', document.getElementById('posCustomer').value);
- formData.append('payment_method', document.getElementById('posPaymentMethod').value);
+ formData.append('customer_id', customerId);
+ formData.append('payments', JSON.stringify(this.payments));
formData.append('total_amount', subtotal);
formData.append('discount_code_id', this.discount ? this.discount.id : '');
formData.append('discount_amount', discountAmount);
@@ -1947,8 +2114,18 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
try {
const resp = await fetch('index.php', { method: 'POST', body: formData });
- const result = await resp.json();
+ const text = await resp.text();
+ let result;
+ try {
+ result = JSON.parse(text);
+ } catch (e) {
+ console.error('Invalid JSON response:', text);
+ throw new Error('Server returned an invalid response');
+ }
+
if (result.success) {
+ const payModal = bootstrap.Modal.getInstance(document.getElementById('posPaymentModal'));
+ if (payModal) payModal.hide();
this.showReceipt(result.invoice_id, discountAmount, loyaltyRedeemed, result.transaction_no);
} else {
Swal.fire('Error', result.error, 'error');
@@ -1957,7 +2134,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
}
} catch (err) {
console.error(err);
- Swal.fire('Error', 'Something went wrong', 'error');
+ Swal.fire('Error', err.message || 'Something went wrong', 'error');
btn.disabled = false;
btn.innerText = originalText;
}
@@ -1965,7 +2142,12 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
showReceipt(invId, discountAmount, loyaltyRedeemed, transactionNo) {
const container = document.getElementById('posReceiptContent');
const customerName = document.getElementById('posCustomer').options[document.getElementById('posCustomer').selectedIndex].text;
- const paymentMethod = document.getElementById('posPaymentMethod').value.toUpperCase();
+ const paymentsHtml = this.payments.map(p => `
+
+ ${p.method}
+ OMR ${p.amount.toFixed(3)}
+
+ `).join('');
const date = new Date().toLocaleString();
let itemsHtml = this.items.map(item => `
@@ -1994,8 +2176,11 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
- Customer: ${customerName}
- Payment: ${paymentMethod}
+ Customer: ${customerName}
+
+
+ Payments:
+ ${paymentsHtml}
@@ -3193,6 +3378,91 @@ document.addEventListener('DOMContentLoaded', function() {
+
+
+
+
+
+
+
+
+
+
Amount Due
+
OMR 0.000
+
+
+
Remaining
+
OMR 0.000
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cash
+
+
+
+ Card
+
+
+
+ Credit
+
+
+
+
+
+
+
+
+
+
+ Total Tendered (Cash)
+ OMR 0.000
+
+
* Change is calculated based on cash payments only.
+
+
+
+
+
+
+