Autosave: 20260219-173848

This commit is contained in:
Flatlogic Bot 2026-02-19 17:38:48 +00:00
parent 552d635c19
commit c9f7fdcaab
8 changed files with 571 additions and 84 deletions

View File

@ -487,7 +487,7 @@ body {
font-weight: 600;
}
.amount-due-box .value {
font-size: 2.5rem;
font-size: 1.25rem;
font-weight: 800;
color: #0f172a;
}

View File

@ -0,0 +1,4 @@
ALTER TABLE system_license ADD COLUMN trial_started_at DATETIME DEFAULT NULL;
INSERT INTO system_license (license_key, status, trial_started_at)
SELECT 'TRIAL', 'pending', NOW()
WHERE NOT EXISTS (SELECT 1 FROM system_license);

View File

@ -0,0 +1,3 @@
-- Fix missing columns for VAT and totals
ALTER TABLE invoice_items ADD COLUMN vat_amount DECIMAL(15,3) DEFAULT 0.000 AFTER unit_price;
ALTER TABLE quotation_items ADD COLUMN vat_amount DECIMAL(15,3) DEFAULT 0.000 AFTER unit_price;

522
index.php
View File

@ -38,9 +38,11 @@ $dir = ($lang === 'ar') ? 'rtl' : 'ltr';
// Licensing Middleware
$is_activated = LicenseService::isActivated();
$trial_days = LicenseService::getTrialRemainingDays();
$can_access = LicenseService::canAccess();
$page = $_GET['page'] ?? 'dashboard';
if (!$is_activated && $page !== 'activate') {
if (!$can_access && $page !== 'activate') {
header("Location: index.php?page=activate");
exit;
}
@ -221,6 +223,43 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
exit;
}
if ($action === 'search_items') {
file_put_contents('search_debug.log', date('Y-m-d H:i:s') . " - search_items call: q=" . ($_GET['q'] ?? '') . "\n", FILE_APPEND);
header('Content-Type: application/json');
$q = $_GET['q'] ?? '';
if (strlen($q) < 1) {
echo json_encode([]);
exit;
}
$searchTerm = "%$q%";
$stmt = db()->prepare("SELECT * FROM stock_items WHERE name_en LIKE ? OR name_ar LIKE ? OR sku LIKE ? LIMIT 15");
$stmt->execute([$searchTerm, $searchTerm, $searchTerm]);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
exit;
}
if ($action === 'get_payments') {
header('Content-Type: application/json');
$invoice_id = (int)$_GET['invoice_id'];
$stmt = db()->prepare("SELECT * FROM payments WHERE invoice_id = ? ORDER BY payment_date DESC");
$stmt->execute([$invoice_id]);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
exit;
}
if ($action === 'get_payment_details') {
header('Content-Type: application/json');
$payment_id = (int)$_GET['payment_id'];
$stmt = db()->prepare("SELECT p.*, i.customer_id, c.name as customer_name
FROM payments p
JOIN invoices i ON p.invoice_id = i.id
JOIN customers c ON i.customer_id = c.id
WHERE p.id = ?");
$stmt->execute([$payment_id]);
echo json_encode($stmt->fetch(PDO::FETCH_ASSOC));
exit;
}
if ($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.created_at DESC");
@ -594,27 +633,51 @@ function getPromotionalPrice($item) {
$inv_date = $_POST['invoice_date'] ?: date('Y-m-d');
$status = $_POST['status'] ?? 'pending';
$pay_type = $_POST['payment_type'] ?? 'cash';
$total_vat = (float)($_POST['total_vat'] ?? 0);
$total_with_vat = (float)($_POST['total_with_vat'] ?? 0);
$paid = (float)($_POST['paid_amount'] ?? 0);
$stmt = $db->prepare("INSERT INTO invoices (customer_id, type, invoice_date, status, payment_type, total_vat, total_with_vat, paid_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$cust_id, $type, $inv_date, $status, $pay_type, $total_vat, $total_with_vat, $paid]);
$inv_id = $db->lastInsertId();
$items = $_POST['items'] ?? [];
$qtys = $_POST['qtys'] ?? [];
$items = $_POST['item_ids'] ?? [];
$qtys = $_POST['quantities'] ?? [];
$prices = $_POST['prices'] ?? [];
$vats = $_POST['vats'] ?? [];
$total_subtotal = 0;
$total_vat = 0;
// First pass to calculate totals
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$total_subtotal += $subtotal;
$total_vat += $vatAmount;
}
$total_with_vat = $total_subtotal + $total_vat;
$paid = (float)($_POST['paid_amount'] ?? 0);
if ($status === 'paid') $paid = $total_with_vat;
$stmt = $db->prepare("INSERT INTO invoices (customer_id, type, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$cust_id, $type, $inv_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid]);
$inv_id = $db->lastInsertId();
$items_for_journal = [];
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$vat = (float)($vats[$i] ?? 0);
$subtotal = $qty * $price;
$db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, vat_amount, subtotal) VALUES (?, ?, ?, ?, ?, ?)")->execute([$inv_id, $item_id, $qty, $price, $vat, $subtotal]);
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$inv_id, $item_id, $qty, $price, $vatAmount, $subtotal]);
$change = ($type === 'sale') ? -$qty : $qty;
$db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?")->execute([$change, $item_id]);
$items_for_journal[] = ['id' => $item_id, 'qty' => $qty];
@ -626,6 +689,168 @@ function getPromotionalPrice($item) {
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}
if (isset($_POST['add_quotation'])) {
$db = db();
try {
$db->beginTransaction();
$cust_id = (int)$_POST['customer_id'];
$quot_date = $_POST['quotation_date'] ?: date('Y-m-d');
$valid_until = $_POST['valid_until'] ?: null;
$items = $_POST['item_ids'] ?? [];
$qtys = $_POST['quantities'] ?? [];
$prices = $_POST['prices'] ?? [];
$total_subtotal = 0;
$total_vat = 0;
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$total_subtotal += $subtotal;
$total_vat += $vatAmount;
}
$total_with_vat = $total_subtotal + $total_vat;
$stmt = $db->prepare("INSERT INTO quotations (customer_id, quotation_date, valid_until, status, total_amount, vat_amount, total_with_vat) VALUES (?, ?, ?, 'pending', ?, ?, ?)");
$stmt->execute([$cust_id, $quot_date, $valid_until, $total_subtotal, $total_vat, $total_with_vat]);
$quot_id = $db->lastInsertId();
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$db->prepare("INSERT INTO quotation_items (quotation_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$quot_id, $item_id, $qty, $price, $vatAmount, $subtotal]);
}
$db->commit();
$message = "Quotation #$quot_id created!";
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}
if (isset($_POST['edit_quotation'])) {
$db = db();
try {
$db->beginTransaction();
$quot_id = (int)$_POST['quotation_id'];
$cust_id = (int)$_POST['customer_id'];
$quot_date = $_POST['quotation_date'];
$valid_until = $_POST['valid_until'] ?: null;
$status = $_POST['status'] ?? 'pending';
$items = $_POST['item_ids'] ?? [];
$qtys = $_POST['quantities'] ?? [];
$prices = $_POST['prices'] ?? [];
$total_subtotal = 0;
$total_vat = 0;
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$total_subtotal += $subtotal;
$total_vat += $vatAmount;
}
$total_with_vat = $total_subtotal + $total_vat;
$stmt = $db->prepare("UPDATE quotations SET customer_id = ?, quotation_date = ?, valid_until = ?, status = ?, total_amount = ?, vat_amount = ?, total_with_vat = ? WHERE id = ?");
$stmt->execute([$cust_id, $quot_date, $valid_until, $status, $total_subtotal, $total_vat, $total_with_vat, $quot_id]);
// Delete old items
$db->prepare("DELETE FROM quotation_items WHERE quotation_id = ?")->execute([$quot_id]);
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$db->prepare("INSERT INTO quotation_items (quotation_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$quot_id, $item_id, $qty, $price, $vatAmount, $subtotal]);
}
$db->commit();
$message = "Quotation #$quot_id updated!";
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}
if (isset($_POST['delete_quotation'])) {
$id = (int)$_POST['id'];
db()->prepare("DELETE FROM quotations WHERE id = ?")->execute([$id]);
db()->prepare("DELETE FROM quotation_items WHERE quotation_id = ?")->execute([$id]);
$message = "Quotation deleted!";
}
if (isset($_POST['convert_to_invoice'])) {
$db = db();
try {
$db->beginTransaction();
$quot_id = (int)$_POST['quotation_id'];
$stmt = $db->prepare("SELECT * FROM quotations WHERE id = ?");
$stmt->execute([$quot_id]);
$quot = $stmt->fetch();
if (!$quot) throw new Exception("Quotation not found.");
if ($quot['status'] === 'converted') throw new Exception("Quotation already converted.");
$stmtItems = $db->prepare("SELECT * FROM quotation_items WHERE quotation_id = ?");
$stmtItems->execute([$quot_id]);
$qItems = $stmtItems->fetchAll();
// Create Invoice
$inv_date = date('Y-m-d');
$stmtInv = $db->prepare("INSERT INTO invoices (customer_id, type, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, 'sale', ?, 'unpaid', 'credit', ?, ?, ?, 0)");
$stmtInv->execute([$quot['customer_id'], $inv_date, $quot['total_amount'], $quot['vat_amount'], $quot['total_with_vat']]);
$inv_id = $db->lastInsertId();
$items_for_journal = [];
foreach ($qItems as $item) {
$db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$inv_id, $item['item_id'], $item['quantity'], $item['unit_price'], $item['vat_amount'], $item['total_price']]);
// Update stock
$db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?")->execute([$item['quantity'], $item['item_id']]);
$items_for_journal[] = ['id' => $item['item_id'], 'qty' => $item['quantity']];
}
// Update Quotation status
$db->prepare("UPDATE quotations SET status = 'converted' WHERE id = ?")->execute([$quot_id]);
// Accounting
recordSaleJournal($inv_id, $quot['total_with_vat'], $inv_date, $items_for_journal, $quot['vat_amount']);
$db->commit();
$message = "Quotation converted to Invoice #$inv_id successfully!";
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}
if (isset($_POST['record_payment'])) {
$inv_id = (int)$_POST['invoice_id'];
$amount = (float)$_POST['amount'];
@ -683,12 +908,78 @@ function getPromotionalPrice($item) {
}
if (isset($_POST['edit_invoice'])) {
$id = (int)$_POST['id'];
$cust_id = (int)$_POST['customer_id'];
$date = $_POST['invoice_date'] ?: date('Y-m-d');
$status = $_POST['status'] ?? 'pending';
db()->prepare("UPDATE invoices SET customer_id = ?, invoice_date = ?, status = ? WHERE id = ?")->execute([$cust_id, $date, $status, $id]);
$message = "Invoice updated!";
$db = db();
try {
$db->beginTransaction();
$id = (int)$_POST['invoice_id'];
$cust_id = (int)$_POST['customer_id'];
$date = $_POST['invoice_date'] ?: date('Y-m-d');
$status = $_POST['status'] ?? 'pending';
$pay_type = $_POST['payment_type'] ?? 'cash';
$items = $_POST['item_ids'] ?? [];
$qtys = $_POST['quantities'] ?? [];
$prices = $_POST['prices'] ?? [];
$total_subtotal = 0;
$total_vat = 0;
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$total_subtotal += $subtotal;
$total_vat += $vatAmount;
}
$total_with_vat = $total_subtotal + $total_vat;
$paid = (float)($_POST['paid_amount'] ?? 0);
if ($status === 'paid') $paid = $total_with_vat;
$db->prepare("UPDATE invoices SET customer_id = ?, invoice_date = ?, status = ?, payment_type = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, paid_amount = ? WHERE id = ?")
->execute([$cust_id, $date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid, $id]);
// Revert stock for old items
$stmtOld = $db->prepare("SELECT ii.item_id, ii.quantity, i.type FROM invoice_items ii JOIN invoices i ON ii.invoice_id = i.id WHERE ii.invoice_id = ?");
$stmtOld->execute([$id]);
$oldItems = $stmtOld->fetchAll();
foreach ($oldItems as $old) {
$change = ($old['type'] === 'sale') ? $old['quantity'] : -$old['quantity'];
$db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?")->execute([$change, $old['item_id']]);
}
// Delete old items
$db->prepare("DELETE FROM invoice_items WHERE invoice_id = ?")->execute([$id]);
// Insert new items and update stock
$inv_type = db()->query("SELECT type FROM invoices WHERE id = $id")->fetchColumn();
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$id, $item_id, $qty, $price, $vatAmount, $subtotal]);
$change = ($inv_type === 'sale') ? -$qty : $qty;
$db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?")->execute([$change, $item_id]);
}
$db->commit();
$message = "Invoice updated successfully!";
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}
// --- HR Handlers ---
@ -1409,7 +1700,20 @@ if ($page === 'export') {
$invType = ($type === 'sales') ? 'sale' : 'purchase';
$where = ["v.type = ?"];
$params = [$invType];
if (!empty($_GET['search'])) { $where[] = "(v.id LIKE ? OR c.name LIKE ?)"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; }
if (!empty($_GET['search'])) {
$s = $_GET['search'];
$clean_id = preg_replace('/[^0-9]/', '', $s);
if ($clean_id !== '') {
$where[] = "(v.id LIKE ? OR c.name LIKE ? OR v.id = ?)";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = $clean_id;
} else {
$where[] = "(v.id LIKE ? OR c.name LIKE ?)";
$params[] = "%$s%";
$params[] = "%$s%";
}
}
if (!empty($_GET['customer_id'])) { $where[] = "v.customer_id = ?"; $params[] = $_GET['customer_id']; }
if (!empty($_GET['start_date'])) { $where[] = "v.invoice_date >= ?"; $params[] = $_GET['start_date']; }
if (!empty($_GET['end_date'])) { $where[] = "v.invoice_date <= ?"; $params[] = $_GET['end_date']; }
@ -1518,9 +1822,18 @@ switch ($page) {
$where = ["1=1"];
$params = [];
if (!empty($_GET['search'])) {
$where[] = "(q.id LIKE ? OR c.name LIKE ?)";
$params[] = "%{$_GET['search']}%";
$params[] = "%{$_GET['search']}%";
$s = $_GET['search'];
$clean_id = preg_replace('/[^0-9]/', '', $s);
if ($clean_id !== '') {
$where[] = "(q.id LIKE ? OR c.name LIKE ? OR q.id = ?)";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = $clean_id;
} else {
$where[] = "(q.id LIKE ? OR c.name LIKE ?)";
$params[] = "%$s%";
$params[] = "%$s%";
}
}
if (!empty($_GET['customer_id'])) {
$where[] = "q.customer_id = ?";
@ -1568,9 +1881,18 @@ switch ($page) {
$params = [$type];
if (!empty($_GET['search'])) {
$where[] = "(v.id LIKE ? OR c.name LIKE ?)";
$params[] = "%{$_GET['search']}%";
$params[] = "%{$_GET['search']}%";
$s = $_GET['search'];
$clean_id = preg_replace('/[^0-9]/', '', $s);
if ($clean_id !== '') {
$where[] = "(v.id LIKE ? OR c.name LIKE ? OR v.id = ?)";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = $clean_id;
} else {
$where[] = "(v.id LIKE ? OR c.name LIKE ?)";
$params[] = "%$s%";
$params[] = "%$s%";
}
}
if (!empty($_GET['customer_id'])) {
@ -1619,10 +1941,21 @@ switch ($page) {
$where = ["1=1"];
$params = [];
if (!empty($_GET['search'])) {
$where[] = "(sr.id LIKE ? OR c.name LIKE ? OR sr.invoice_id LIKE ?)";
$params[] = "%{$_GET['search']}%";
$params[] = "%{$_GET['search']}%";
$params[] = "%{$_GET['search']}%";
$s = $_GET['search'];
$clean_id = preg_replace('/[^0-9]/', '', $s);
if ($clean_id !== '') {
$where[] = "(sr.id LIKE ? OR c.name LIKE ? OR sr.invoice_id LIKE ? OR sr.id = ? OR sr.invoice_id = ?)";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = $clean_id;
$params[] = $clean_id;
} else {
$where[] = "(sr.id LIKE ? OR c.name LIKE ? OR sr.invoice_id LIKE ?)";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = "%$s%";
}
}
$whereSql = implode(" AND ", $where);
$stmt = db()->prepare("SELECT sr.*, c.name as customer_name, i.total_with_vat as invoice_total
@ -1640,10 +1973,21 @@ switch ($page) {
$where = ["1=1"];
$params = [];
if (!empty($_GET['search'])) {
$where[] = "(pr.id LIKE ? OR c.name LIKE ? OR pr.invoice_id LIKE ?)";
$params[] = "%{$_GET['search']}%";
$params[] = "%{$_GET['search']}%";
$params[] = "%{$_GET['search']}%";
$s = $_GET['search'];
$clean_id = preg_replace('/[^0-9]/', '', $s);
if ($clean_id !== '') {
$where[] = "(pr.id LIKE ? OR c.name LIKE ? OR pr.invoice_id LIKE ? OR pr.id = ? OR pr.invoice_id = ?)";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = $clean_id;
$params[] = $clean_id;
} else {
$where[] = "(pr.id LIKE ? OR c.name LIKE ? OR pr.invoice_id LIKE ?)";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = "%$s%";
}
}
$whereSql = implode(" AND ", $where);
$stmt = db()->prepare("SELECT pr.*, c.name as supplier_name, i.total_with_vat as invoice_total
@ -2019,6 +2363,14 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
</head>
<body class="theme-<?= htmlspecialchars($_SESSION['theme'] ?? 'default') ?>">
<?php if (!$is_activated && $trial_days > 0): ?>
<div class="alert alert-warning text-center mb-0 rounded-0 d-print-none py-2" style="position: sticky; top: 0; z-index: 2000; font-size: 0.85rem;">
<i class="bi bi-info-circle-fill me-2"></i>
<?= $lang === 'ar' ? "نسخة تجريبية: متبقي $trial_days يوم" : "Trial Version: $trial_days days remaining" ?>.
<a href="index.php?page=activate" class="alert-link ms-2 fw-bold"><?= __('activate_now') ?></a>
</div>
<?php endif; ?>
<div class="sidebar">
<div class="sidebar-header"><?= __('accounting') ?></div>
<nav class="mt-4">
@ -3807,7 +4159,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
this.payments = [];
this.renderPayments();
document.getElementById('paymentAmountDue').innerText = '<?= __('currency') ?> ' + total.toFixed(3);
document.getElementById('paymentAmountDue').innerText = total.toFixed(3);
document.getElementById('partialAmount').value = total.toFixed(3);
// Sync credit customer selection if credit is default or already selected
@ -3894,7 +4246,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
<div class="payment-line">
<div>
<span class="method">${methodLabels[p.method] || p.method}</span>
<span class="ms-2 badge bg-secondary small"><?= __('currency') ?> ${p.amount.toFixed(3)}</span>
<span class="ms-2 badge bg-secondary small">${p.amount.toFixed(3)}</span>
</div>
<button class="btn btn-sm btn-outline-danger border-0" onclick="cart.removePaymentLine(${i})">
<i class="bi bi-trash"></i>
@ -3906,7 +4258,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
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);
display.innerText = 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);
@ -3917,7 +4269,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
const changeDisplay = document.getElementById('changeDue');
if (changeDisplay) {
changeDisplay.innerText = 'OMR ' + displayChange.toFixed(3);
changeDisplay.innerText = 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';
@ -8237,8 +8589,8 @@ document.addEventListener('DOMContentLoaded', function() {
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_invoice_total').value = total.toFixed(3);
document.getElementById('pay_remaining_amount').value = remaining.toFixed(3);
document.getElementById('pay_amount').value = remaining.toFixed(3);
document.getElementById('pay_amount').max = remaining.toFixed(3);
});
@ -8266,7 +8618,7 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('receiptCustomer').textContent = data.customer_name || '---';
document.getElementById('receiptInvNo').textContent = 'INV-' + data.inv_id.toString().padStart(5, '0');
document.getElementById('receiptMethod').textContent = data.payment_method;
document.getElementById('receiptAmount').textContent = 'OMR ' + parseFloat(data.amount).toFixed(3);
document.getElementById('receiptAmount').textContent = parseFloat(data.amount).toFixed(3);
document.getElementById('receiptAmountWords').textContent = data.amount_words;
// Update labels for Purchase vs Sale
@ -8392,14 +8744,14 @@ document.addEventListener('DOMContentLoaded', function() {
searchInput.addEventListener('input', function() {
clearTimeout(timeout);
const q = this.value.trim();
if (q.length < 1) {
if (q.length < 2) {
suggestions.style.display = 'none';
return;
}
timeout = setTimeout(() => {
fetch(`index.php?action=search_items&q=${encodeURIComponent(q)}`)
.then(res => res.json())
.then(res => res.ok ? res.json() : [])
.then(data => {
suggestions.innerHTML = '';
if (data.length > 0) {
@ -9997,33 +10349,33 @@ document.addEventListener('DOMContentLoaded', function() {
<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 class="mb-2">
<label class="form-label smaller fw-bold mb-1" data-en="Total Amount" data-ar="المبلغ الإجمالي">Total Amount</label>
<input type="text" id="pay_invoice_total" class="form-control form-control-sm" 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 class="mb-2">
<label class="form-label smaller fw-bold mb-1" data-en="Remaining Amount" data-ar="المبلغ المتبقي">Remaining Amount</label>
<input type="text" id="pay_remaining_amount" class="form-control form-control-sm" 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 class="mb-2">
<label class="form-label smaller fw-bold mb-1" 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 form-control-sm" 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 class="mb-2">
<label class="form-label smaller fw-bold mb-1" data-en="Payment Date" data-ar="تاريخ الدفع">Payment Date</label>
<input type="date" name="payment_date" class="form-control form-control-sm" 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 select2" required>
<div class="mb-2">
<label class="form-label smaller fw-bold mb-1" data-en="Payment Method" data-ar="طريقة الدفع">Payment Method</label>
<select name="payment_method" class="form-select form-select-sm select2" required>
<option value="Cash">Cash</option>
<option value="Card">Credit 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 class="mb-2">
<label class="form-label smaller fw-bold mb-1" data-en="Notes" data-ar="ملاحظات">Notes</label>
<textarea name="notes" class="form-control form-control-sm" rows="2"></textarea>
</div>
</div>
<div class="modal-footer">
@ -10082,7 +10434,7 @@ document.addEventListener('DOMContentLoaded', function() {
</div>
<div class="text-center my-4 py-4 border-top border-bottom">
<p class="mb-1 text-muted small text-uppercase fw-bold">Amount Paid / المبلغ المدفوع</p>
<h1 class="display-5 fw-bold text-primary mb-1" id="receiptAmount"></h1>
<h3 class="fw-bold text-primary mb-1" id="receiptAmount"></h3>
<p class="text-muted small font-italic" id="receiptAmountWords"></p>
</div>
<div id="receiptNotesContainer" class="mb-4" style="display: none;">
@ -10107,6 +10459,35 @@ document.addEventListener('DOMContentLoaded', function() {
</div>
</div>
<style>
#posPaymentModal .modal-body { padding: 0.75rem; font-size: 0.8rem; }
#posPaymentModal .amount-due-box { background: #f8f9fa; border-radius: 12px; padding: 10px 0; border: 1px solid #eee; }
#posPaymentModal .amount-due-box .label { font-size: 0.65rem; text-transform: uppercase; font-weight: 700; color: #64748b; }
#posPaymentModal .amount-due-box .value { font-size: 1rem; font-weight: 800; color: #1e293b; }
#posPaymentModal .payment-methods-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
#posPaymentModal .payment-method-btn {
padding: 6px 4px; border-radius: 10px; border: 2px solid #f1f5f9; cursor: pointer;
text-align: center; transition: all 0.2s; background: white;
}
#posPaymentModal .payment-method-btn.active { border-color: #3b82f6; background: #eff6ff; color: #1d4ed8; }
#posPaymentModal .payment-method-btn i { font-size: 1rem; display: block; margin-bottom: 2px; }
#posPaymentModal .payment-method-btn span { font-size: 0.65rem; }
#posPaymentModal .quick-pay-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; }
#posPaymentModal .quick-pay-btn {
padding: 5px; border-radius: 8px; border: 1px solid #e2e8f0; background: white;
font-weight: bold; text-align: center; cursor: pointer; font-size: 0.75rem;
}
#posPaymentModal .payment-line {
display: flex; justify-content: space-between; align-items: center;
padding: 6px 10px; background: #f1f5f9; border-radius: 8px; margin-bottom: 4px;
}
#posPaymentModal .payment-line .method { font-weight: 600; color: #475569; font-size: 0.75rem; }
#posPaymentModal .form-control { font-size: 0.85rem; padding: 0.35rem 0.6rem; }
#posPaymentModal .btn-primary { padding: 0.35rem 0.8rem; font-size: 0.85rem; }
#posPaymentModal .modal-header { padding: 0.75rem 1rem 0.25rem; }
#posPaymentModal .modal-footer { padding: 0.25rem 1rem 0.75rem; }
</style>
<!-- POS Payment Modal -->
<div class="modal fade" id="posPaymentModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
@ -10116,7 +10497,7 @@ 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="mb-2 p-2 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>
@ -10135,25 +10516,25 @@ document.addEventListener('DOMContentLoaded', function() {
</div>
</div>
<div class="amount-due-box mb-3">
<div class="amount-due-box mb-2">
<div class="d-flex justify-content-between px-3">
<div class="text-start">
<div class="label">Amount Due</div>
<div class="value" id="paymentAmountDue"><?= __('currency') ?> 0.000</div>
<div class="value" id="paymentAmountDue">0.000</div>
</div>
<div class="text-end">
<div class="label text-danger">Remaining</div>
<div class="value text-danger" id="paymentRemaining"><?= __('currency') ?> 0.000</div>
<div class="value text-danger" id="paymentRemaining">0.000</div>
</div>
</div>
</div>
<div id="paymentList" class="mb-3">
<div id="paymentList" class="mb-2">
<!-- Added payments will appear here -->
</div>
<div class="mb-3 p-3 border rounded bg-light">
<label class="form-label small fw-bold">Add Payment Method</label>
<div class="mb-2 p-2 border rounded bg-light">
<label class="form-label small fw-bold mb-1">Add Payment Method</label>
<div class="payment-methods-grid mb-2">
<div class="payment-method-btn active" data-method="cash" onclick="cart.selectMethod('cash', this)">
<i class="bi bi-cash-stack"></i>
@ -10177,7 +10558,6 @@ document.addEventListener('DOMContentLoaded', function() {
<div class="col">
<label class="form-label smaller fw-bold mb-1">Amount</label>
<div class="input-group">
<span class="input-group-text p-1 small">OMR</span>
<input type="number" step="0.001" id="partialAmount" class="form-control" placeholder="0.000" oninput="cart.updateRemaining()">
</div>
</div>
@ -10200,7 +10580,7 @@ document.addEventListener('DOMContentLoaded', function() {
<div id="cashPaymentSection" style="display: none;">
<div class="d-flex justify-content-between align-items-center p-3 bg-primary-subtle rounded border border-primary-subtle">
<span class="fw-bold">Total Tendered (Cash)</span>
<span class="h4 m-0 fw-bold text-primary" id="changeDue"><?= __('currency') ?> 0.000</span>
<span class="h6 m-0 fw-bold text-primary" id="changeDue">0.000</span>
</div>
<div class="small text-muted mt-1">* Change is calculated based on cash payments only.</div>
</div>

View File

@ -1,23 +1,74 @@
<?php
class LicenseService {
private static $remote_api_url = 'https://licensing.your-domain.com/api/v1'; // Replace with your server
private static $remote_api_url = null;
private static function getApiUrl() {
if (self::$remote_api_url === null) {
self::$remote_api_url = getenv('LICENSE_API_URL') ?: 'https://omanapp.cloud/meezan_register';
}
return self::$remote_api_url;
}
/**
* Returns the number of days remaining in the trial.
*/
public static function getTrialRemainingDays() {
require_once __DIR__ . '/../db/config.php';
$stmt = db()->prepare("SELECT trial_started_at FROM system_license LIMIT 1");
$stmt->execute();
$res = $stmt->fetch();
if (!$res || !$res['trial_started_at']) {
self::startTrial();
return 15;
}
$started = strtotime($res['trial_started_at']);
$days_elapsed = floor((time() - $started) / (24 * 60 * 60));
return (int) max(0, 15 - $days_elapsed);
}
/**
* Initializes the trial period.
*/
private static function startTrial() {
require_once __DIR__ . '/../db/config.php';
$stmt = db()->prepare("SELECT COUNT(*) FROM system_license");
$stmt->execute();
if ($stmt->fetchColumn() == 0) {
db()->exec("INSERT INTO system_license (status, trial_started_at) VALUES ('trial', NOW())");
} else {
db()->exec("UPDATE system_license SET trial_started_at = NOW() WHERE trial_started_at IS NULL");
}
}
/**
* Generates a unique fingerprint for this server environment.
*/
public static function getFingerprint() {
$data = [
php_uname(),
php_uname('n'), // Nodename (hostname)
php_uname('m'), // Machine type
$_SERVER['SERVER_ADDR'] ?? '127.0.0.1',
$_SERVER['HTTP_HOST'] ?? 'localhost',
PHP_OS
];
return hash('sha256', implode('|', $data));
}
/**
* Checks if the system is currently activated.
* Checks if the system is currently activated or within trial period.
*/
public static function canAccess() {
if (self::isActivated()) return true;
$daysLeft = self::getTrialRemainingDays();
return $daysLeft > 0;
}
/**
* Checks if the system is currently activated with a valid license key.
*/
public static function isActivated() {
require_once __DIR__ . '/../db/config.php';
@ -90,11 +141,10 @@ class LicenseService {
* Remote API Caller
*/
private static function callRemoteApi($endpoint, $params) {
$url = self::$remote_api_url . $endpoint;
$url = self::getApiUrl() . $endpoint;
// Check if we are in local development / simulation mode
// If the URL is still the default placeholder, we use simulation
if (strpos(self::$remote_api_url, 'your-domain.com') !== false) {
if (strpos($url, 'your-domain.com') !== false) {
return self::simulateApi($endpoint, $params);
}
@ -125,13 +175,31 @@ class LicenseService {
*/
private static function simulateApi($endpoint, $params) {
$clean_key = strtoupper(trim($params['license_key'] ?? ''));
if (strpos($clean_key, 'FLAT-') === 0) {
if ($endpoint === '/verify') return ['success' => true];
// Strict format check: FLAT-XXXX-XXXX-XXXX (where X is hex)
$pattern = '/^FLAT-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}$/';
if (!preg_match($pattern, $clean_key)) {
return [
'success' => true,
'activation_token' => hash('sha256', $params['license_key'] . $params['fingerprint'] . 'DEBUG_SALT')
'success' => false,
'error' => 'Invalid License Format. Expected FLAT-XXXX-XXXX-XXXX (Simulation Mode).'
];
}
return ['success' => false, 'error' => 'License key invalid or expired (Simulation Mode).'];
// In a real server, you would check if this key is already used by a DIFFERENT fingerprint.
// To simulate a "Max Activations Reached" error, you can use a specific key:
if ($clean_key === 'FLAT-0000-0000-0000') {
return [
'success' => false,
'error' => 'Activation Failed: This license key is already active on another machine.'
];
}
if ($endpoint === '/verify') return ['success' => true];
return [
'success' => true,
'activation_token' => hash('sha256', $params['license_key'] . $params['fingerprint'] . 'DEBUG_SALT')
];
}
}

View File

@ -89,3 +89,18 @@
2026-02-19 10:54:25 - POST: {"open_register":"1","register_id":"2","opening_balance":"5"}
2026-02-19 10:54:25 - POST: {"open_register":"1","register_id":"2","opening_balance":"5"}
2026-02-19 10:54:26 - POST: {"open_register":"1","register_id":"2","opening_balance":"5"}
2026-02-19 11:26:49 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":0.845}]","total_amount":"0.8450000000000001","tax_amount":"0.010119047619047618","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":3,\"qty\":1,\"price\":0.25,\"vat_rate\":0,\"vat_amount\":0},{\"id\":1,\"qty\":1,\"price\":0.3825,\"vat_rate\":0,\"vat_amount\":0},{\"id\":2,\"qty\":1,\"price\":0.2125,\"vat_rate\":5,\"vat_amount\":0.010119047619047618}]"}
2026-02-19 11:27:38 - POST: {"type":"sale","customer_id":"4","invoice_date":"2026-02-19","payment_type":"cash","status":"unpaid","paid_amount":"0.000","add_invoice":""}
2026-02-19 11:48:37 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":1}]","total_amount":"0.2125","tax_amount":"0.010119047619047618","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":2,\"qty\":1,\"price\":0.2125,\"vat_rate\":5,\"vat_amount\":0.010119047619047618}]"}
2026-02-19 12:56:11 - POST: {"type":"purchase","customer_id":"5","invoice_date":"2026-02-19","payment_type":"cash","status":"unpaid","paid_amount":"0.000","item_ids":["1"],"quantities":["1"],"prices":["0.400"],"add_invoice":""}
2026-02-19 12:56:41 - POST: {"type":"purchase","customer_id":"5","invoice_date":"2026-02-19","payment_type":"cash","status":"unpaid","paid_amount":"0.000","item_ids":["1"],"quantities":["1"],"prices":["0.400"],"add_invoice":""}
2026-02-19 12:57:01 - POST: {"type":"purchase","customer_id":"5","invoice_date":"2026-02-19","payment_type":"cash","status":"unpaid","paid_amount":"0.000","item_ids":["1"],"quantities":["1"],"prices":["0.400"],"add_invoice":""}
2026-02-19 12:57:13 - POST: {"type":"purchase","customer_id":"5","invoice_date":"2026-02-19","payment_type":"cash","status":"unpaid","paid_amount":"0.000","item_ids":["1"],"quantities":["1"],"prices":["0.400"],"add_invoice":""}
2026-02-19 12:57:25 - POST: {"type":"purchase","customer_id":"5","invoice_date":"2026-02-19","payment_type":"cash","status":"unpaid","paid_amount":"0.000","item_ids":["1"],"quantities":["1"],"prices":["0.400"],"add_invoice":""}
2026-02-19 13:13:37 - POST: {"type":"purchase","customer_id":"5","invoice_date":"2026-02-19","payment_type":"cash","status":"unpaid","paid_amount":"0.000","item_ids":["1"],"quantities":["1"],"prices":["0.400"],"add_invoice":""}
2026-02-19 13:14:31 - POST: {"type":"purchase","customer_id":"6","invoice_date":"2026-02-19","payment_type":"cash","status":"unpaid","paid_amount":"0.000","item_ids":["1"],"quantities":["1"],"prices":["0.400"],"add_invoice":""}
2026-02-19 13:14:55 - POST: {"type":"sale","customer_id":"4","invoice_date":"2026-02-19","payment_type":"cash","status":"unpaid","paid_amount":"0.000","item_ids":["1"],"quantities":["1"],"prices":["0.450"],"add_invoice":""}
2026-02-19 13:59:29 - POST: {"type":"sale","customer_id":"4","invoice_date":"2026-02-19","payment_type":"cash","status":"unpaid","paid_amount":"0.000","item_ids":["1"],"quantities":["1"],"prices":["0.450"],"add_invoice":""}
2026-02-19 14:55:10 - POST: {"type":"sale","customer_id":"4","invoice_date":"2026-02-19","payment_type":"cash","status":"unpaid","paid_amount":"0.000","item_ids":["1"],"quantities":["1"],"prices":["0.450"],"add_invoice":""}
2026-02-19 14:55:32 - POST: {"type":"sale","customer_id":"4","invoice_date":"2026-02-19","payment_type":"cash","status":"paid","paid_amount":"0.000","item_ids":["1"],"quantities":["1"],"prices":["0.450"],"add_invoice":""}
2026-02-19 15:06:48 - POST: {"type":"sale","customer_id":"4","invoice_date":"2026-02-19","payment_type":"cash","status":"paid","paid_amount":"0.000","item_ids":["1"],"quantities":["1"],"prices":["0.450"],"add_invoice":""}

2
search_debug.log Normal file
View File

@ -0,0 +1,2 @@
2026-02-19 13:59:25 - search_items call: q=to
2026-02-19 14:55:24 - search_items call: q=to

15
test_search.php Normal file
View File

@ -0,0 +1,15 @@
<?php
require_once 'db/config.php';
$q = 'Tomato';
$searchTerm = "%$q%";
$stmt = db()->prepare("SELECT * FROM stock_items WHERE name_en LIKE ? OR name_ar LIKE ? OR sku LIKE ? LIMIT 15");
$stmt->execute([$searchTerm, $searchTerm, $searchTerm]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo "Results for Tomato: " . count($results) . "\n";
print_r($results);
$q = 'INV-00001';
$searchTerm = "%$q%";
$stmt->execute([$searchTerm, $searchTerm, $searchTerm]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo "Results for INV-00001: " . count($results) . "\n";