Autosave: 20260219-173848
This commit is contained in:
parent
552d635c19
commit
c9f7fdcaab
@ -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;
|
||||
}
|
||||
|
||||
4
db/migrations/20260219_add_trial_logic.sql
Normal file
4
db/migrations/20260219_add_trial_logic.sql
Normal 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);
|
||||
3
db/migrations/20260219_fix_vat_columns.sql
Normal file
3
db/migrations/20260219_fix_vat_columns.sql
Normal 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
522
index.php
@ -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>
|
||||
|
||||
@ -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')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
2
search_debug.log
Normal 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
15
test_search.php
Normal 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";
|
||||
Loading…
x
Reference in New Issue
Block a user