last changes

This commit is contained in:
Flatlogic Bot 2026-02-25 17:48:02 +00:00
parent bda20a7ffb
commit e3c02137ea
19 changed files with 824 additions and 34 deletions

View File

@ -5,3 +5,9 @@
2026-02-25 11:49:27 - Items case hit
2026-02-25 11:51:57 - Items case hit
2026-02-25 12:41:41 - Items case hit
2026-02-25 12:45:17 - Items case hit
2026-02-25 13:33:02 - Items case hit
2026-02-25 14:04:18 - Items case hit
2026-02-25 14:06:09 - Items case hit
2026-02-25 14:10:50 - Items case hit
2026-02-25 15:28:54 - Items case hit

30
fix_duplicates.php Normal file
View File

@ -0,0 +1,30 @@
<?php
$content = file_get_contents('index.php');
$search1 = '<div class="mb-3">
<label class="form-label fw-semibold" data-en="Assigned Outlet" data-ar="الفرع">Assigned Outlet</label>
<select name="outlet_id" class="form-select">
<option value="">--- Global (Admin only) ---</option>
<?php foreach (($data[\'outlets\'] ?? []) as $o): ?>
<option value="<?= $o[\'id\'] ?>" <?= ($u[\'outlet_id\'] ?? null) == $o[\'id\'] ? \'selected\' : \'\' ?>><?= htmlspecialchars($o[\'name\']) ?></option>
<?php endforeach; ?>
</select>
</div>';
$content = str_replace($search1 . "\n " . $search1, $search1, $content);
$search2 = '<div class="mb-3">
<label class="form-label" data-en="Assigned Outlet" data-ar="الفرع">Assigned Outlet</label>
<select name="outlet_id" class="form-select">
<option value="">--- Global (Admin only) ---</option>
<?php foreach (($data[\'outlets\'] ?? []) as $o): ?>
<option value="<?= $o[\'id\'] ?>"><?= htmlspecialchars($o[\'name\']) ?></option>
<?php endforeach; ?>
</select>
</div>';
$content = str_replace($search2 . "\n " . $search2, $search2, $content);
file_put_contents('index.php', $content);
echo "Duplicates removed.\n";

11
fix_login_dup.php Normal file
View File

@ -0,0 +1,11 @@
<?php
$content = file_get_contents('index.php');
$search = "\$_SESSION['outlet_id'] = \$u['outlet_id'];";
$replacement = "\$_SESSION['outlet_id'] = \$u['outlet_id'];";
// To fix duplicates, we can simply replace 3 of them with 1
$content = preg_replace("/(\s*\\\$_\SESSION\['outlet_id'\] = \\\$u\['outlet_id'\];){2,}/", "\n \$_SESSION['outlet_id'] = \$u['outlet_id'];", $content);
file_put_contents('index.php', $content);
echo "Duplicate login outlet_ids removed.\n";

2
fix_ui_typo.php Normal file
View File

@ -0,0 +1,2 @@
<?php
// Just a final check to remove any leftovers from previous attempts if any

36
fix_users_table.php Normal file
View File

@ -0,0 +1,36 @@
<?php
$content = file_get_contents('index.php');
$search_th = '<th data-en="Access Level" data-ar="مستوى الوصول">Access Level</th>';
$replace_th = '<th data-en="Access Level" data-ar="مستوى الوصول">Access Level</th>
<th data-en="Outlet" data-ar="الفرع">Outlet</th>';
$content = str_replace($search_th, $replace_th, $content);
$search_td = '<span class="badge rounded-pill bg-info bg-opacity-10 text-info px-3">
<?= htmlspecialchars((string)($u[\'group_name\'] ?? \'No Role Assigned\')) ?>
</span>
</td>';
$replace_td = '<span class="badge rounded-pill bg-info bg-opacity-10 text-info px-3">
<?= htmlspecialchars((string)($u[\'group_name\'] ?? \'No Role Assigned\')) ?>
</span>
</td>
<td>
<span class="badge rounded-pill bg-secondary bg-opacity-10 text-secondary px-3">
<?php
$out_name = "Global / All Outlets";
foreach (($data["outlets"] ?? []) as $out) {
if ($out["id"] == $u["outlet_id"]) {
$out_name = $out["name"];
break;
}
}
echo htmlspecialchars($out_name);
?>
</span>
</td>';
$content = str_replace($search_td, $replace_td, $content);
file_put_contents('index.php', $content);
echo "Users table updated to show assigned outlet.\n";

View File

@ -194,3 +194,14 @@ function getVatReport($start_date = null, $end_date = null) {
'net_vat' => $output_vat - $input_vat
];
}
/**
* Alias for Payroll Expense Journal
*/
function recordExpenseJournalForPayroll($payroll_id, $amount, $date) {
$entries = [
['code' => '5200', 'debit' => $amount],
['code' => '1100', 'credit' => $amount]
];
return createJournalEntry($date, "Payroll Expense #$payroll_id", "PAYROLL-$payroll_id", 'payroll', $payroll_id, $entries);
}

400
index.php
View File

@ -344,6 +344,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
$_SESSION['user_id'] = $u['id'];
$_SESSION['username'] = $u['username'];
$_SESSION['user_role_name'] = $u['role_name'];
$_SESSION['outlet_id'] = $u['outlet_id'];
// Fetch permissions from the new role_permissions table
$permStmt = db()->prepare("SELECT permission FROM role_permissions WHERE role_id = ?");
@ -2658,9 +2659,10 @@ if (isset($_POST['add_hr_department'])) {
$group_id = (int)($_POST['group_id'] ?? 0) ?: null;
if ($username && $password) {
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$stmt = db()->prepare("INSERT INTO users (username, password, email, phone, group_id) VALUES (?, ?, ?, ?, ?)");
$outlet_id = !empty($_POST['outlet_id']) ? (int)$_POST['outlet_id'] : null;
$stmt = db()->prepare("INSERT INTO users (username, password, email, phone, group_id, outlet_id) VALUES (?, ?, ?, ?, ?, ?)");
try {
$stmt->execute([$username, $hashed_password, $email, $phone, $group_id]);
$stmt->execute([$username, $hashed_password, $email, $phone, $group_id, $outlet_id]);
$message = "User added successfully!";
} catch (PDOException $e) {
if ($e->getCode() == '23000') {
@ -2715,9 +2717,10 @@ if (isset($_POST['add_hr_department'])) {
$phone = $_POST['phone'] ?? '';
$group_id = (int)($_POST['group_id'] ?? 0) ?: null;
$status = $_POST['status'] ?? 'active';
$outlet_id = !empty($_POST['outlet_id']) ? (int)$_POST['outlet_id'] : null;
if ($id && $username) {
$stmt = db()->prepare("UPDATE users SET username = ?, email = ?, phone = ?, group_id = ?, status = ? WHERE id = ?");
$stmt->execute([$username, $email, $phone, $group_id, $status, $id]);
$stmt = db()->prepare("UPDATE users SET username = ?, email = ?, phone = ?, group_id = ?, status = ?, outlet_id = ? WHERE id = ?");
$stmt->execute([$username, $email, $phone, $group_id, $status, $outlet_id, $id]);
if (!empty($_POST['password'])) {
$hashed_password = password_hash($_POST['password'], PASSWORD_DEFAULT);
@ -2885,6 +2888,37 @@ if (isset($_POST['add_hr_department'])) {
}
}
// --- Outlets Handlers ---
if (isset($_POST['add_outlet']) && ($_SESSION['user_role_name'] ?? '') === 'Administrator') {
$name = $_POST['name'] ?? '';
$phone = $_POST['phone'] ?? '';
$address = $_POST['address'] ?? '';
$status = $_POST['status'] ?? 'active';
if ($name) {
$stmt = db()->prepare("INSERT INTO outlets (name, phone, address, status) VALUES (?, ?, ?, ?)");
$stmt->execute([$name, $phone, $address, $status]);
redirectWithMessage("Outlet added successfully!", "index.php?page=outlets");
}
}
if (isset($_POST['edit_outlet']) && ($_SESSION['user_role_name'] ?? '') === 'Administrator') {
$id = (int)$_POST['id'];
$name = $_POST['name'] ?? '';
$phone = $_POST['phone'] ?? '';
$address = $_POST['address'] ?? '';
$status = $_POST['status'] ?? 'active';
if ($name) {
$stmt = db()->prepare("UPDATE outlets SET name=?, phone=?, address=?, status=? WHERE id=?");
$stmt->execute([$name, $phone, $address, $status, $id]);
redirectWithMessage("Outlet updated successfully!", "index.php?page=outlets");
}
}
if (isset($_POST['delete_outlet']) && ($_SESSION['user_role_name'] ?? '') === 'Administrator') {
$id = (int)$_POST['id'];
$stmt = db()->prepare("DELETE FROM outlets WHERE id=?");
$stmt->execute([$id]);
redirectWithMessage("Outlet deleted successfully!", "index.php?page=outlets");
}
// --- Cash Register & Session Handlers ---
if (isset($_POST['add_cash_register'])) {
$name = $_POST['name'] ?? '';
@ -3531,6 +3565,9 @@ switch ($page) {
break;
case 'quotations':
$where = ["1=1"];
if (isset($_SESSION['outlet_id'])) {
$where[] = "q.outlet_id = " . (int)$_SESSION['outlet_id'];
}
$params = [];
if (!empty($_GET['search'])) {
$s = $_GET['search'];
@ -3579,6 +3616,9 @@ switch ($page) {
break;
case 'lpos':
$where = ["1=1"];
if (isset($_SESSION['outlet_id'])) {
$where[] = "q.outlet_id = " . (int)$_SESSION['outlet_id'];
}
$params = [];
if (!empty($_GET['search'])) {
$s = $_GET['search'];
@ -3644,6 +3684,9 @@ switch ($page) {
$cust_supplier_table = ($type === 'purchase') ? 'suppliers' : 'customers';
$where = ["1=1"];
if (isset($_SESSION['outlet_id'])) {
$where[] = "v.outlet_id = " . (int)$_SESSION['outlet_id'];
}
$params = [];
if (!empty($_GET['search'])) {
@ -3721,6 +3764,9 @@ switch ($page) {
case 'sales_returns':
$where = ["1=1"];
if (isset($_SESSION['outlet_id'])) {
$where[] = "sr.outlet_id = " . (int)$_SESSION['outlet_id'];
}
$params = [];
if (!empty($_GET['search'])) {
$s = $_GET['search'];
@ -3753,6 +3799,9 @@ switch ($page) {
case 'purchase_returns':
$where = ["1=1"];
if (isset($_SESSION['outlet_id'])) {
$where[] = "pr.outlet_id = " . (int)$_SESSION['outlet_id'];
}
$params = [];
if (!empty($_GET['search'])) {
$s = $_GET['search'];
@ -3826,6 +3875,9 @@ switch ($page) {
break;
case 'expenses':
$where = ["1=1"];
if (isset($_SESSION['outlet_id'])) {
$where[] = "e.outlet_id = " . (int)$_SESSION['outlet_id'];
}
$params = [];
if (!empty($_GET['category_id'])) {
$where[] = "e.category_id = ?";
@ -3854,6 +3906,7 @@ switch ($page) {
case 'users':
$data['users'] = db()->query("SELECT u.*, g.name as group_name FROM users u LEFT JOIN role_groups g ON u.group_id = g.id ORDER BY u.username ASC")->fetchAll();
$data['role_groups'] = db()->query("SELECT id, name FROM role_groups ORDER BY name ASC")->fetchAll();
$data['outlets'] = db()->query("SELECT id, name FROM outlets ORDER BY name ASC")->fetchAll();
break;
case 'backups':
$data['backups'] = BackupService::getBackups();
@ -4099,24 +4152,72 @@ switch ($page) {
break;
default:
if (can('dashboard_view')) {
$data['customers'] = db()->query("SELECT * FROM customers ORDER BY id DESC LIMIT 5")->fetchAll();
$out_w = isset($_SESSION['outlet_id']) ? "WHERE outlet_id = " . (int)$_SESSION['outlet_id'] : "WHERE 1=1";
$out_and = isset($_SESSION['outlet_id']) ? "AND outlet_id = " . (int)$_SESSION['outlet_id'] : "";
$data['customers'] = db()->query("SELECT * FROM customers $out_w ORDER BY id DESC LIMIT 5")->fetchAll();
$data['stats'] = [
'total_customers' => db()->query("SELECT COUNT(*) FROM customers")->fetchColumn(),
'total_items' => db()->query("SELECT COUNT(*) FROM stock_items")->fetchColumn(),
'total_sales' => (db()->query("SELECT SUM(total_with_vat) FROM invoices")->fetchColumn() ?: 0) + (db()->query("SELECT SUM(net_amount) FROM pos_transactions WHERE status = 'completed'")->fetchColumn() ?: 0),
'total_received' => (db()->query("SELECT SUM(amount) FROM payments")->fetchColumn() ?: 0) + (db()->query("SELECT SUM(amount) FROM pos_payments")->fetchColumn() ?: 0),
'total_purchases' => db()->query("SELECT SUM(total_with_vat) FROM purchases")->fetchColumn() ?: 0,
'total_paid' => db()->query("SELECT SUM(amount) FROM purchase_payments")->fetchColumn() ?: 0,
'expired_items' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date <= CURDATE()")->fetchColumn(),
'near_expiry_items' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date > CURDATE() AND expiry_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY)")->fetchColumn(),
'low_stock_items_count' => db()->query("SELECT COUNT(*) FROM stock_items WHERE stock_quantity <= min_stock_level")->fetchColumn(),
'total_customers' => db()->query("SELECT COUNT(*) FROM customers $out_w")->fetchColumn(),
'total_items' => db()->query("SELECT COUNT(*) FROM stock_items $out_w")->fetchColumn(),
'total_sales' => (db()->query("SELECT SUM(total_with_vat) FROM invoices $out_w")->fetchColumn() ?: 0) + (db()->query("SELECT SUM(net_amount) FROM pos_transactions WHERE status = 'completed' $out_and")->fetchColumn() ?: 0),
'total_received' => (db()->query("SELECT SUM(amount) FROM payments $out_w")->fetchColumn() ?: 0) + (db()->query("SELECT SUM(amount) FROM pos_payments WHERE 1=1 $out_and")->fetchColumn() ?: 0),
'total_purchases' => db()->query("SELECT SUM(total_with_vat) FROM purchases $out_w")->fetchColumn() ?: 0,
'total_paid' => db()->query("SELECT SUM(amount) FROM purchase_payments $out_w")->fetchColumn() ?: 0,
'expired_items' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date <= CURDATE() $out_and")->fetchColumn(),
'near_expiry_items' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date > CURDATE() AND expiry_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY) $out_and")->fetchColumn(),
'low_stock_items_count' => db()->query("SELECT COUNT(*) FROM stock_items WHERE stock_quantity <= min_stock_level $out_and")->fetchColumn(),
];
$data['stats']['total_receivable'] = $data['stats']['total_sales'] - $data['stats']['total_received'];
$data['stats']['total_payable'] = $data['stats']['total_purchases'] - $data['stats']['total_paid'];
// Sales Chart Data
$data['monthly_sales'] = db()->query("SELECT DATE_FORMAT(invoice_date, '%M %Y') as label, SUM(total_with_vat) as total FROM invoices GROUP BY DATE_FORMAT(invoice_date, '%Y-%m') ORDER BY invoice_date ASC LIMIT 12")->fetchAll(PDO::FETCH_ASSOC);
$data['yearly_sales'] = db()->query("SELECT YEAR(invoice_date) as label, SUM(total_with_vat) as total FROM invoices GROUP BY label ORDER BY label ASC LIMIT 5")->fetchAll(PDO::FETCH_ASSOC);
// Sales Chart Data (Invoices + POS)
$data['monthly_sales'] = db()->query("
SELECT label, SUM(tot) as total FROM (
SELECT DATE_FORMAT(invoice_date, '%M %Y') as label, total_with_vat as tot, DATE_FORMAT(invoice_date, '%Y-%m') as sort_col FROM invoices $out_w
UNION ALL
SELECT DATE_FORMAT(created_at, '%M %Y') as label, net_amount as tot, DATE_FORMAT(created_at, '%Y-%m') as sort_col FROM pos_transactions WHERE status = 'completed' $out_and
) t
GROUP BY label, sort_col
ORDER BY sort_col ASC LIMIT 12
")->fetchAll(PDO::FETCH_ASSOC);
$data['yearly_sales'] = db()->query("
SELECT label, SUM(tot) as total FROM (
SELECT YEAR(invoice_date) as label, total_with_vat as tot FROM invoices $out_w
UNION ALL
SELECT YEAR(created_at) as label, net_amount as tot FROM pos_transactions WHERE status = 'completed' $out_and
) t
GROUP BY label
ORDER BY label ASC LIMIT 5
")->fetchAll(PDO::FETCH_ASSOC);
// Cash Flow Data (Income vs Expense - last 6 months)
$data['cash_flow'] = db()->query("
SELECT m.sort_col, m.label,
(
SELECT COALESCE(SUM(amount), 0) FROM payments WHERE DATE_FORMAT(payment_date, '%Y-%m') = m.sort_col $out_and
) + (
SELECT COALESCE(SUM(amount), 0) FROM pos_payments WHERE DATE_FORMAT(created_at, '%Y-%m') = m.sort_col $out_and
) as income,
(
SELECT COALESCE(SUM(amount), 0) FROM expenses WHERE DATE_FORMAT(expense_date, '%Y-%m') = m.sort_col $out_and
) + (
SELECT COALESCE(SUM(amount), 0) FROM purchase_payments WHERE DATE_FORMAT(payment_date, '%Y-%m') = m.sort_col $out_and
) + (
SELECT COALESCE(SUM(net_salary), 0) FROM hr_payroll WHERE DATE_FORMAT(payment_date, '%Y-%m') = m.sort_col $out_and
) as expense
FROM (
SELECT DISTINCT DATE_FORMAT(dt, '%Y-%m') as sort_col, DATE_FORMAT(dt, '%M %Y') as label
FROM (
SELECT payment_date as dt FROM payments $out_w
UNION SELECT created_at as dt FROM pos_payments $out_w
UNION SELECT expense_date as dt FROM expenses $out_w
UNION SELECT payment_date as dt FROM purchase_payments $out_w
) dates
) m
ORDER BY m.sort_col DESC LIMIT 6
")->fetchAll(PDO::FETCH_ASSOC);
$data['cash_flow'] = array_reverse($data['cash_flow']); // Chronological order
}
break;
}
@ -4144,6 +4245,8 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?= time() ?>"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
@ -4257,6 +4360,71 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
body { font-size: 12px !important; color: #000 !important; background: #fff !important; }
@page { margin: 1cm; }
}
/* Thermal Receipt Layout */
.thermal-receipt {
width: 300px;
margin: 0 auto;
background: #fff;
color: #000;
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
line-height: 1.4;
padding: 10px;
}
.thermal-receipt .center {
text-align: center;
}
.thermal-receipt .separator {
border-bottom: 1px dashed #000;
margin: 8px 0;
}
.thermal-receipt table {
width: 100%;
border-collapse: collapse;
}
.thermal-receipt table th, .thermal-receipt table td {
padding: 4px 2px;
text-align: left;
vertical-align: top;
}
.thermal-receipt table th {
border-bottom: 1px dashed #000;
font-weight: bold;
}
.thermal-receipt .total-row {
font-weight: bold;
font-size: 14px;
border-top: 1px dashed #000;
border-bottom: 1px dashed #000;
padding: 5px 0;
margin: 5px 0;
}
.thermal-receipt.rtl {
direction: rtl;
text-align: right;
}
.thermal-receipt.rtl table th, .thermal-receipt.rtl table td {
text-align: right;
}
@media print {
body.printing-receipt {
background: none;
}
body.printing-receipt .thermal-receipt-print {
width: 100% !important;
max-width: 300px; /* typically 80mm thermal paper width */
margin: 0;
padding: 0;
}
body.printing-receipt #posPrintArea {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
}
.print-only { display: none; }
[dir="rtl"] { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
</style>
@ -4513,12 +4681,17 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
<?php endif; ?>
<!-- Administrations Section -->
<?php if (can('users_view')): ?>
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['role_groups', 'users', 'cash_registers', 'register_sessions', 'scale_devices', 'backups', 'customer_display_settings']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#admin-collapse">
<?php if (can('users_view') || ($_SESSION['user_role_name'] ?? '') === 'Administrator'): ?>
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['role_groups', 'users', 'cash_registers', 'register_sessions', 'scale_devices', 'backups', 'customer_display_settings', 'outlets']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#admin-collapse">
<span><i class="fas fa-user-gear group-icon"></i><span><?= __('admin') ?></span></span>
<i class="fas fa-chevron-down chevron"></i>
</div>
<div class="collapse <?= in_array($page, ['role_groups', 'users', 'cash_registers', 'register_sessions', 'scale_devices', 'backups', 'customer_display_settings']) ? 'show' : '' ?>" id="admin-collapse">
<div class="collapse <?= in_array($page, ['role_groups', 'users', 'cash_registers', 'register_sessions', 'scale_devices', 'backups', 'customer_display_settings', 'outlets']) ? 'show' : '' ?>" id="admin-collapse">
<?php if (($_SESSION['user_role_name'] ?? '') === 'Administrator'): ?>
<a href="index.php?page=outlets" class="nav-link <?= $page === 'outlets' ? 'active' : '' ?>">
<i class="fas fa-shop"></i> <span>Outlets</span>
</a>
<?php endif; ?>
<a href="index.php?page=role_groups" class="nav-link <?= $page === 'role_groups' ? 'active' : '' ?>">
<i class="fas fa-user-shield"></i> <span><?= __('role_groups') ?></span>
</a>
@ -4847,20 +5020,31 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
</div>
<div class="row g-4 mb-4">
<div class="col-12">
<div class="card p-4">
<div class="col-md-7">
<div class="card p-4 h-100 shadow-sm border-0" style="border-radius: 12px;">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="m-0" data-en="Sales Performance" data-ar="أداء المبيعات">Sales Performance</h5>
<div class="btn-group btn-group-sm">
<h5 class="m-0 fw-bold text-dark" data-en="Sales Trend" data-ar="اتجاه المبيعات">Sales Trend</h5>
<div class="btn-group btn-group-sm shadow-sm">
<button type="button" class="btn btn-outline-primary active" id="btnMonthly" data-en="Monthly" data-ar="شهري">Monthly</button>
<button type="button" class="btn btn-outline-primary" id="btnYearly" data-en="Yearly" data-ar="سنوي">Yearly</button>
</div>
</div>
<div style="height: 300px;">
<div style="height: 300px; position: relative;">
<canvas id="salesChart"></canvas>
</div>
</div>
</div>
<div class="col-md-5">
<div class="card p-4 h-100 shadow-sm border-0" style="border-radius: 12px;">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="m-0 fw-bold text-dark" data-en="Cash Flow Overview" data-ar="نظرة عامة على التدفق النقدي">Cash Flow Overview</h5>
<span class="badge bg-light text-muted border" data-en="Last 6 Months" data-ar="آخر 6 أشهر">Last 6 Months</span>
</div>
<div style="height: 300px; position: relative;">
<canvas id="cashFlowChart"></canvas>
</div>
</div>
</div>
</div>
<div class="row">
@ -10005,6 +10189,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
<tr>
<th class="ps-4" data-en="User Info" data-ar="معلومات المستخدم">User Info</th>
<th data-en="Access Level" data-ar="مستوى الوصول">Access Level</th>
<th data-en="Outlet" data-ar="الفرع">Outlet</th>
<th data-en="Contact" data-ar="الاتصال">Contact</th>
<th data-en="Status" data-ar="الحالة">Status</th>
<th data-en="Actions" data-ar="الإجراءات" class="text-end pe-4">Actions</th>
@ -10033,6 +10218,20 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
<?= htmlspecialchars((string)($u['group_name'] ?? 'No Role Assigned')) ?>
</span>
</td>
<td>
<span class="badge rounded-pill bg-secondary bg-opacity-10 text-secondary px-3">
<?php
$out_name = "Global / All Outlets";
foreach (($data["outlets"] ?? []) as $out) {
if ($out["id"] == $u["outlet_id"]) {
$out_name = $out["name"];
break;
}
}
echo htmlspecialchars($out_name);
?>
</span>
</td>
<td>
<div class="text-dark small mb-1"><i class="bi bi-envelope me-1"></i> <?= htmlspecialchars((string)($u['email'] ?? '')) ?></div>
<div class="text-muted small"><i class="bi bi-phone me-1"></i> <?= htmlspecialchars((string)($u['phone'] ?? '-')) ?></div>
@ -10097,6 +10296,15 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label fw-semibold" data-en="Assigned Outlet" data-ar="الفرع">Assigned Outlet</label>
<select name="outlet_id" class="form-select">
<option value="">--- Global (Admin only) ---</option>
<?php foreach (($data['outlets'] ?? []) as $o): ?>
<option value="<?= $o['id'] ?>" <?= ($u['outlet_id'] ?? null) == $o['id'] ? 'selected' : '' ?>><?= htmlspecialchars($o['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label fw-semibold" data-en="Account Status" data-ar="حالة الحساب">Account Status</label>
<select name="status" class="form-select">
@ -11045,6 +11253,15 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label" data-en="Assigned Outlet" data-ar="الفرع">Assigned Outlet</label>
<select name="outlet_id" class="form-select">
<option value="">--- Global (Admin only) ---</option>
<?php foreach (($data['outlets'] ?? []) as $o): ?>
<option value="<?= $o['id'] ?>"><?= htmlspecialchars($o['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
@ -13302,6 +13519,7 @@ document.addEventListener('DOMContentLoaded', function() {
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-en="Close" data-ar="إغلاق">Close</button>
<button type="button" class="btn btn-primary" onclick="window.printLPO()">Print LPO</button>
<button type="button" class="btn btn-danger" onclick="downloadPDF('lpoDetailsContent', 'LPO')"><i class="bi bi-file-earmark-pdf me-2"></i>PDF</button>
</div>
</div>
</div>
@ -13488,6 +13706,7 @@ document.addEventListener('DOMContentLoaded', function() {
<div class="modal-footer d-print-none">
<div id="quotationActionButtons" class="me-auto"></div>
<button type="button" class="btn btn-secondary" onclick="window.print()"><i class="bi bi-printer"></i data-en="Print" data-ar="طباعة">Print</button>
<button type="button" class="btn btn-danger" onclick="downloadPDF('quotationPrintableArea', 'Quotation')"><i class="bi bi-file-earmark-pdf me-2"></i>PDF</button>
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Close" data-ar="إغلاق">Close</button>
</div>
</div>
@ -13726,6 +13945,7 @@ document.addEventListener('DOMContentLoaded', function() {
<div class="modal-footer d-print-none">
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Close" data-ar="إغلاق">Close</button>
<button type="button" class="btn btn-primary" onclick="window.print()"><i class="bi bi-printer me-2"></i>Print Invoice</button>
<button type="button" class="btn btn-danger" onclick="downloadPDF('invoicePrintableArea', 'Invoice')"><i class="bi bi-file-earmark-pdf me-2"></i>PDF</button>
</div>
</div>
</div>
@ -14622,6 +14842,29 @@ document.addEventListener('DOMContentLoaded', function() {
location.reload();
}
window.downloadPDF = function(elementId, filenamePrefix) {
const element = document.getElementById(elementId);
if (!element) return;
// Hide elements with 'd-print-none' during PDF generation
const hiddenElements = element.querySelectorAll('.d-print-none');
hiddenElements.forEach(el => el.style.display = 'none');
const opt = {
margin: [10, 10, 10, 10],
filename: filenamePrefix + '_' + new Date().getTime() + '.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
html2pdf().set(opt).from(element).save().then(() => {
// Restore hidden elements
hiddenElements.forEach(el => el.style.display = '');
});
};
// --- Language Apply Script ---
function applyLanguage(node) {
const docLang = document.documentElement.lang || 'ar';
@ -14682,19 +14925,31 @@ document.addEventListener('DOMContentLoaded', function() {
});
// -----------------------------
<?php if ($page === 'dashboard' && can('dashboard_view')): ?>
const monthlyData = <?= json_encode($data['monthly_sales']) ?>;
const yearlyData = <?= json_encode($data['yearly_sales']) ?>;
const monthlyData = <?= json_encode($data['monthly_sales'] ?? []) ?>;
const yearlyData = <?= json_encode($data['yearly_sales'] ?? []) ?>;
const cashFlowData = <?= json_encode($data['cash_flow'] ?? []) ?>;
const ctx = document.getElementById('salesChart').getContext('2d');
let gradient = ctx.createLinearGradient(0, 0, 0, 400);
gradient.addColorStop(0, 'rgba(13, 110, 253, 0.4)');
gradient.addColorStop(1, 'rgba(13, 110, 253, 0)');
let salesChart = new Chart(ctx, {
type: 'line',
data: {
labels: monthlyData.map(d => d.label),
datasets: [{
label: 'Sales (OMR)',
label: '<?= $lang === "ar" ? "المبيعات" : "Sales" ?>',
data: monthlyData.map(d => d.total),
borderColor: '#0d6efd',
backgroundColor: 'rgba(13, 110, 253, 0.1)',
backgroundColor: gradient,
borderWidth: 3,
pointBackgroundColor: '#ffffff',
pointBorderColor: '#0d6efd',
pointBorderWidth: 2,
pointRadius: 4,
pointHoverRadius: 6,
fill: true,
tension: 0.4
}]
@ -14703,16 +14958,36 @@ document.addEventListener('DOMContentLoaded', function() {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(0,0,0,0.8)',
titleFont: { size: 14, family: "'Segoe UI', Roboto, sans-serif" },
bodyFont: { size: 14, family: "'Segoe UI', Roboto, sans-serif" },
padding: 10,
cornerRadius: 8,
displayColors: false,
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) { label += ': '; }
if (context.parsed.y !== null) { label += new Intl.NumberFormat().format(context.parsed.y) + ' OMR'; }
return label;
}
}
}
},
scales: {
x: { grid: { display: false, drawBorder: false } },
y: {
beginAtZero: true,
grid: { color: 'rgba(0,0,0,0.05)', drawBorder: false },
border: { dash: [5, 5] },
ticks: {
callback: function(value) { return 'OMR ' + value.toFixed(3); }
}
}
}
},
interaction: { mode: 'index', intersect: false }
}
});
@ -14731,6 +15006,69 @@ document.addEventListener('DOMContentLoaded', function() {
salesChart.data.datasets[0].data = yearlyData.map(d => d.total);
salesChart.update();
});
if (cashFlowData.length > 0) {
const ctxCf = document.getElementById('cashFlowChart').getContext('2d');
new Chart(ctxCf, {
type: 'bar',
data: {
labels: cashFlowData.map(d => d.label),
datasets: [
{
label: '<?= $lang === "ar" ? "الدخل" : "Income" ?>',
data: cashFlowData.map(d => d.income),
backgroundColor: '#198754',
borderRadius: 4,
barPercentage: 0.6,
categoryPercentage: 0.8
},
{
label: '<?= $lang === "ar" ? "المصروفات" : "Expenses" ?>',
data: cashFlowData.map(d => d.expense),
backgroundColor: '#dc3545',
borderRadius: 4,
barPercentage: 0.6,
categoryPercentage: 0.8
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
labels: { usePointStyle: true, boxWidth: 8, font: { family: "'Segoe UI', Roboto, sans-serif" } }
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.8)',
padding: 10,
cornerRadius: 8,
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) { label += ': '; }
if (context.parsed.y !== null) { label += new Intl.NumberFormat().format(context.parsed.y) + ' OMR'; }
return label;
}
}
}
},
scales: {
x: { grid: { display: false, drawBorder: false } },
y: {
beginAtZero: true,
grid: { color: 'rgba(0,0,0,0.05)', drawBorder: false },
border: { dash: [5, 5] },
ticks: {
callback: function(value) { return 'OMR ' + value.toFixed(3); }
}
}
},
interaction: { mode: 'index', intersect: false }
}
});
}
<?php endif; ?>
</script>

60
patch_cashflow.php Normal file
View File

@ -0,0 +1,60 @@
<?php
$c = file_get_contents('index.php');
$find_cf = ' // Cash Flow Data (Income vs Expense - last 6 months)
$data[\'cash_flow\'] = db()->query("
SELECT m.sort_col, m.label,
(
SELECT COALESCE(SUM(amount), 0) FROM payments WHERE DATE_FORMAT(payment_date, \'%Y-%m\') = m.sort_col
) + (
SELECT COALESCE(SUM(amount), 0) FROM pos_payments WHERE DATE_FORMAT(created_at, \'%Y-%m\') = m.sort_col
) as income,
(
SELECT COALESCE(SUM(amount), 0) FROM expenses WHERE DATE_FORMAT(expense_date, \'%Y-%m\') = m.sort_col
) + (
SELECT COALESCE(SUM(amount), 0) FROM purchase_payments WHERE DATE_FORMAT(payment_date, \'%Y-%m\') = m.sort_col
) + (
SELECT COALESCE(SUM(net_salary), 0) FROM hr_payroll WHERE DATE_FORMAT(payment_date, \'%Y-%m\') = m.sort_col
) as expense
FROM (
SELECT DISTINCT DATE_FORMAT(dt, \'%Y-%m\') as sort_col, DATE_FORMAT(dt, \'%M %Y\') as label
FROM (
SELECT payment_date as dt FROM payments
UNION SELECT created_at as dt FROM pos_payments
UNION SELECT expense_date as dt FROM expenses
UNION SELECT payment_date as dt FROM purchase_payments
) dates
) m
ORDER BY m.sort_col DESC LIMIT 6
")->fetchAll(PDO::FETCH_ASSOC);';
$repl_cf = ' // Cash Flow Data (Income vs Expense - last 6 months)
$data[\'cash_flow\'] = db()->query("
SELECT m.sort_col, m.label,
(
SELECT COALESCE(SUM(amount), 0) FROM payments WHERE DATE_FORMAT(payment_date, \'%Y-%m\') = m.sort_col $out_and
) + (
SELECT COALESCE(SUM(amount), 0) FROM pos_payments WHERE DATE_FORMAT(created_at, \'%Y-%m\') = m.sort_col $out_and
) as income,
(
SELECT COALESCE(SUM(amount), 0) FROM expenses WHERE DATE_FORMAT(expense_date, \'%Y-%m\') = m.sort_col $out_and
) + (
SELECT COALESCE(SUM(amount), 0) FROM purchase_payments WHERE DATE_FORMAT(payment_date, \'%Y-%m\') = m.sort_col $out_and
) + (
SELECT COALESCE(SUM(net_salary), 0) FROM hr_payroll WHERE DATE_FORMAT(payment_date, \'%Y-%m\') = m.sort_col $out_and
) as expense
FROM (
SELECT DISTINCT DATE_FORMAT(dt, \'%Y-%m\') as sort_col, DATE_FORMAT(dt, \'%M %Y\') as label
FROM (
SELECT payment_date as dt FROM payments $out_w
UNION SELECT created_at as dt FROM pos_payments $out_w
UNION SELECT expense_date as dt FROM expenses $out_w
UNION SELECT payment_date as dt FROM purchase_payments $out_w
) dates
) m
ORDER BY m.sort_col DESC LIMIT 6
")->fetchAll(PDO::FETCH_ASSOC);';
$c = str_replace($find_cf, $repl_cf, $c);
file_put_contents('index.php', $c);
echo "Cash flow patched\n";

81
patch_dashboard.php Normal file
View File

@ -0,0 +1,81 @@
<?php
$c = file_get_contents('index.php');
$find_dashboard = ' if (can(\'dashboard_view\')) {
$data[\'customers\'] = db()->query("SELECT * FROM customers ORDER BY id DESC LIMIT 5")->fetchAll();
$data[\'stats\'] = [
\'total_customers\' => db()->query("SELECT COUNT(*) FROM customers")->fetchColumn(),
\'total_items\' => db()->query("SELECT COUNT(*) FROM stock_items")->fetchColumn(),
\'total_sales\' => (db()->query("SELECT SUM(total_with_vat) FROM invoices")->fetchColumn() ?: 0) + (db()->query("SELECT SUM(net_amount) FROM pos_transactions WHERE status = \'completed\'")->fetchColumn() ?: 0),
\'total_received\' => (db()->query("SELECT SUM(amount) FROM payments")->fetchColumn() ?: 0) + (db()->query("SELECT SUM(amount) FROM pos_payments")->fetchColumn() ?: 0),
\'total_purchases\' => db()->query("SELECT SUM(total_with_vat) FROM purchases")->fetchColumn() ?: 0,
\'total_paid\' => db()->query("SELECT SUM(amount) FROM purchase_payments")->fetchColumn() ?: 0,
\'expired_items\' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date <= CURDATE()")->fetchColumn(),
\'near_expiry_items\' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date > CURDATE() AND expiry_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY)")->fetchColumn(),
\'low_stock_items_count\' => db()->query("SELECT COUNT(*) FROM stock_items WHERE stock_quantity <= min_stock_level")->fetchColumn(),
];';
$repl_dashboard = ' if (can(\'dashboard_view\')) {
$out_w = isset($_SESSION[\'outlet_id\']) ? "WHERE outlet_id = " . (int)$_SESSION[\'outlet_id\'] : "WHERE 1=1";
$out_and = isset($_SESSION[\'outlet_id\']) ? "AND outlet_id = " . (int)$_SESSION[\'outlet_id\'] : "";
$data[\'customers\'] = db()->query("SELECT * FROM customers $out_w ORDER BY id DESC LIMIT 5")->fetchAll();
$data[\'stats\'] = [
\'total_customers\' => db()->query("SELECT COUNT(*) FROM customers $out_w")->fetchColumn(),
\'total_items\' => db()->query("SELECT COUNT(*) FROM stock_items $out_w")->fetchColumn(),
\'total_sales\' => (db()->query("SELECT SUM(total_with_vat) FROM invoices $out_w")->fetchColumn() ?: 0) + (db()->query("SELECT SUM(net_amount) FROM pos_transactions WHERE status = \'completed\' $out_and")->fetchColumn() ?: 0),
\'total_received\' => (db()->query("SELECT SUM(amount) FROM payments $out_w")->fetchColumn() ?: 0) + (db()->query("SELECT SUM(amount) FROM pos_payments WHERE 1=1 $out_and")->fetchColumn() ?: 0),
\'total_purchases\' => db()->query("SELECT SUM(total_with_vat) FROM purchases $out_w")->fetchColumn() ?: 0,
\'total_paid\' => db()->query("SELECT SUM(amount) FROM purchase_payments $out_w")->fetchColumn() ?: 0,
\'expired_items\' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date <= CURDATE() $out_and")->fetchColumn(),
\'near_expiry_items\' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date > CURDATE() AND expiry_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY) $out_and")->fetchColumn(),
\'low_stock_items_count\' => db()->query("SELECT COUNT(*) FROM stock_items WHERE stock_quantity <= min_stock_level $out_and")->fetchColumn(),
];';
$c = str_replace($find_dashboard, $repl_dashboard, $c);
// Also replace the queries for Cash Flow and Charts
$find_charts = ' $data[\'monthly_sales\'] = db()->query("
SELECT label, SUM(tot) as total FROM (
SELECT DATE_FORMAT(invoice_date, \'%M %Y\') as label, total_with_vat as tot, DATE_FORMAT(invoice_date, \'%Y-%m\') as sort_col FROM invoices
UNION ALL
SELECT DATE_FORMAT(created_at, \'%M %Y\') as label, net_amount as tot, DATE_FORMAT(created_at, \'%Y-%m\') as sort_col FROM pos_transactions WHERE status = \'completed\'
) t
GROUP BY label, sort_col
ORDER BY sort_col ASC LIMIT 12
")->fetchAll(PDO::FETCH_ASSOC);
$data[\'yearly_sales\'] = db()->query("
SELECT label, SUM(tot) as total FROM (
SELECT YEAR(invoice_date) as label, total_with_vat as tot FROM invoices
UNION ALL
SELECT YEAR(created_at) as label, net_amount as tot FROM pos_transactions WHERE status = \'completed\'
) t
GROUP BY label
ORDER BY label ASC LIMIT 5
")->fetchAll(PDO::FETCH_ASSOC);';
$repl_charts = ' $data[\'monthly_sales\'] = db()->query("
SELECT label, SUM(tot) as total FROM (
SELECT DATE_FORMAT(invoice_date, \'%M %Y\') as label, total_with_vat as tot, DATE_FORMAT(invoice_date, \'%Y-%m\') as sort_col FROM invoices $out_w
UNION ALL
SELECT DATE_FORMAT(created_at, \'%M %Y\') as label, net_amount as tot, DATE_FORMAT(created_at, \'%Y-%m\') as sort_col FROM pos_transactions WHERE status = \'completed\' $out_and
) t
GROUP BY label, sort_col
ORDER BY sort_col ASC LIMIT 12
")->fetchAll(PDO::FETCH_ASSOC);
$data[\'yearly_sales\'] = db()->query("
SELECT label, SUM(tot) as total FROM (
SELECT YEAR(invoice_date) as label, total_with_vat as tot FROM invoices $out_w
UNION ALL
SELECT YEAR(created_at) as label, net_amount as tot FROM pos_transactions WHERE status = \'completed\' $out_and
) t
GROUP BY label
ORDER BY label ASC LIMIT 5
")->fetchAll(PDO::FETCH_ASSOC);';
$c = str_replace($find_charts, $repl_charts, $c);
file_put_contents('index.php', $c);
echo "Dashboard Patched\n";

9
patch_expenses.php Normal file
View File

@ -0,0 +1,9 @@
<?php
$content = file_get_contents('index.php');
$search = "case 'expenses':\n \$where = [\"1=1\"];";
$replace = "case 'expenses':\n \$where = [\"1=1\"];\n if (isset(\$_SESSION['outlet_id'])) {\n \$where[] = \"e.outlet_id = \" . (int)\$_SESSION['outlet_id'];\n }";
$content = str_replace($search, $replace, $content);
file_put_contents('index.php', $content);
echo "Expenses patched.\n";

15
patch_lists.php Normal file
View File

@ -0,0 +1,15 @@
<?php
$content = file_get_contents('index.php');
$search = '$where = ["1=1"];';
$replace = '$where = ["1=1"];
if (isset($_SESSION[\'outlet_id\'])) {
$where[] = "v.outlet_id = " . (int)$_SESSION[\'outlet_id\'];
}';
// Apply to case 'sales', 'purchases'
$content = preg_replace("/case 'sales':.*?case 'purchases':.*?\\\$where = \[\"1=1\"\];/s", "case 'sales':\n case 'purchases':\n \$type = (\$page === 'sales') ? 'sale' : 'purchase';\n \$table = (\$type === 'purchase') ? 'purchases' : 'invoices';\n \$cust_supplier_col = (\$type === 'purchase') ? 'supplier_id' : 'customer_id';\n \$cust_supplier_table = (\$type === 'purchase') ? 'suppliers' : 'customers';\n \n \$where = [\"1=1\"];\n if (isset(\$_SESSION['outlet_id'])) {\n \$where[] = \"v.outlet_id = \" . (int)\$_SESSION['outlet_id'];\n }", $content);
file_put_contents('index.php', $content);
echo "sales and purchases patched.\n";

25
patch_others.php Normal file
View File

@ -0,0 +1,25 @@
<?php
$content = file_get_contents('index.php');
// Quotations
$search_q = "case 'quotations':\n \$where = [\"1=1\"];";
$replace_q = "case 'quotations':\n \$where = [\"1=1\"];\n if (isset(\$_SESSION['outlet_id'])) {\n \$where[] = \"q.outlet_id = \" . (int)\$_SESSION['outlet_id'];\n }";
$content = str_replace($search_q, $replace_q, $content);
// LPOs
$search_l = "case 'lpos':\n \$where = [\"1=1\"];";
$replace_l = "case 'lpos':\n \$where = [\"1=1\"];\n if (isset(\$_SESSION['outlet_id'])) {\n \$where[] = \"q.outlet_id = \" . (int)\$_SESSION['outlet_id'];\n }";
$content = str_replace($search_l, $replace_l, $content);
// Sales Returns
$search_sr = "case 'sales_returns':\n \$where = [\"1=1\"];";
$replace_sr = "case 'sales_returns':\n \$where = [\"1=1\"];\n if (isset(\$_SESSION['outlet_id'])) {\n \$where[] = \"sr.outlet_id = \" . (int)\$_SESSION['outlet_id'];\n }";
$content = str_replace($search_sr, $replace_sr, $content);
// Purchase Returns
$search_pr = "case 'purchase_returns':\n \$where = [\"1=1\"];";
$replace_pr = "case 'purchase_returns':\n \$where = [\"1=1\"];\n if (isset(\$_SESSION['outlet_id'])) {\n \$where[] = \"pr.outlet_id = \" . (int)\$_SESSION['outlet_id'];\n }";
$content = str_replace($search_pr, $replace_pr, $content);
file_put_contents('index.php', $content);
echo "Other lists patched.\n";

26
patch_outlets.php Normal file
View File

@ -0,0 +1,26 @@
<?php
$content = file_get_contents('index.php');
// 1. Fix Login Session
$content = str_replace(
"\$_SESSION['user_role_name'] = \$u['role_name'];",
"\$_SESSION['user_role_name'] = \$u['role_name'];\n \$_SESSION['outlet_id'] = \$u['outlet_id'];",
$content
);
// 2. Fix Add User POST
$content = preg_replace(
"/\\\$group_id = \(int\)\(\\\$_POST\['group_id'\] \?\? 0\) \?\: null;.*?\\\$stmt = \\\$db->prepare\(\"INSERT INTO users \(username, password, email, phone, group_id\) VALUES \(\?, \?, \?, \?, \?\)\"\);.*?\\\$stmt->execute\(\[\\\$username, password_hash\(\\\$password, PASSWORD_DEFAULT\), \\\$email, \\\$phone, \\\$group_id\]\);/s",
"\$group_id = (int)(\$_POST['group_id'] ?? 0) ?: null;\n \$outlet_id = !empty(\$_POST['outlet_id']) ? (int)\$_POST['outlet_id'] : null;\n \$stmt = \$db->prepare(\"INSERT INTO users (username, password, email, phone, group_id, outlet_id) VALUES (?, ?, ?, ?, ?, ?)\");\n \$stmt->execute([\$username, password_hash(\$password, PASSWORD_DEFAULT), \$email, \$phone, \$group_id, \$outlet_id]);",
$content
);
// 3. Fix Edit User POST
$content = preg_replace(
"/\\\$group_id = \(int\)\(\\\$_POST\['group_id'\] \?\? 0\) \?\: null;.*?if \(\!empty\(\\\$password\)\) \{.*?\\\$stmt = \\\$db->prepare\(\"UPDATE users SET username=\?, password=\?, email=\?, phone=\?, group_id=\? WHERE id=\?\"\);.*?\\\$stmt->execute\(\[\\\$username, password_hash\(\\\$password, PASSWORD_DEFAULT\), \\\$email, \\\$phone, \\\$group_id, \\\$id\]\);.*?\} else \{.*?\\\$stmt = \\\$db->prepare\(\"UPDATE users SET username=\?, email=\?, phone=\?, group_id=\? WHERE id=\?\"\);.*?\\\$stmt->execute\(\[\\\$username, \\\$email, \\\$phone, \\\$group_id, \\\$id\]\);.*?\}/s",
"\$group_id = (int)(\$_POST['group_id'] ?? 0) ?: null;\n \$outlet_id = !empty(\$_POST['outlet_id']) ? (int)\$_POST['outlet_id'] : null;\n if (!empty(\$password)) {\n \$stmt = \$db->prepare(\"UPDATE users SET username=?, password=?, email=?, phone=?, group_id=?, outlet_id=? WHERE id=?\");\n \$stmt->execute([\$username, password_hash(\$password, PASSWORD_DEFAULT), \$email, \$phone, \$group_id, \$outlet_id, \$id]);\n } else {\n \$stmt = \$db->prepare(\"UPDATE users SET username=?, email=?, phone=?, group_id=?, outlet_id=? WHERE id=?\");\n \$stmt->execute([\$username, \$email, \$phone, \$group_id, \$outlet_id, \$id]);\n }",
$content
);
file_put_contents('index.php', $content);
echo "Patched auth & post logic\n";

46
patch_users.php Normal file
View File

@ -0,0 +1,46 @@
<?php
$c = file_get_contents('index.php');
// Fix Add User
$find_add = ' $stmt = db()->prepare("INSERT INTO users (username, password, email, phone, group_id) VALUES (?, ?, ?, ?, ?)");
try {
$stmt->execute([$username, $hashed_password, $email, $phone, $group_id]);';
$repl_add = ' $outlet_id = !empty($_POST[\'outlet_id\']) ? (int)$_POST[\'outlet_id\'] : null;
$stmt = db()->prepare("INSERT INTO users (username, password, email, phone, group_id, outlet_id) VALUES (?, ?, ?, ?, ?, ?)");
try {
$stmt->execute([$username, $hashed_password, $email, $phone, $group_id, $outlet_id]);';
$c = str_replace($find_add, $repl_add, $c);
// Fix Edit User
$find_edit = ' if ($password) {
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$stmt = db()->prepare("UPDATE users SET username = ?, password = ?, email = ?, phone = ?, group_id = ? WHERE id = ?");
$stmt->execute([$username, $hashed_password, $email, $phone, $group_id, $id]);
} else {
$stmt = db()->prepare("UPDATE users SET username = ?, email = ?, phone = ?, group_id = ? WHERE id = ?");
$stmt->execute([$username, $email, $phone, $group_id, $id]);
}';
$repl_edit = ' $outlet_id = !empty($_POST[\'outlet_id\']) ? (int)$_POST[\'outlet_id\'] : null;
if ($password) {
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$stmt = db()->prepare("UPDATE users SET username = ?, password = ?, email = ?, phone = ?, group_id = ?, outlet_id = ? WHERE id = ?");
$stmt->execute([$username, $hashed_password, $email, $phone, $group_id, $outlet_id, $id]);
} else {
$stmt = db()->prepare("UPDATE users SET username = ?, email = ?, phone = ?, group_id = ?, outlet_id = ? WHERE id = ?");
$stmt->execute([$username, $email, $phone, $group_id, $outlet_id, $id]);
}';
$c = str_replace($find_edit, $repl_edit, $c);
// Fix login session
$c = str_replace(
"\$_SESSION['user_role_name'] = \$u['role_name'];",
"\$_SESSION['user_role_name'] = \$u['role_name'];\n \$_SESSION['outlet_id'] = \$u['outlet_id'];",
$c
);
file_put_contents('index.php', $c);
echo "Patched users.php\n";

17
patch_users_edit_post.php Normal file
View File

@ -0,0 +1,17 @@
<?php
$c = file_get_contents('index.php');
$find_edit_post = '$status = $_POST[\'status\'] ?? \'active\';
if ($id && $username) {
$stmt = db()->prepare("UPDATE users SET username = ?, email = ?, phone = ?, group_id = ?, status = ? WHERE id = ?");
$stmt->execute([$username, $email, $phone, $group_id, $status, $id]);';
$repl_edit_post = '$status = $_POST[\'status\'] ?? \'active\';
$outlet_id = !empty($_POST[\'outlet_id\']) ? (int)$_POST[\'outlet_id\'] : null;
if ($id && $username) {
$stmt = db()->prepare("UPDATE users SET username = ?, email = ?, phone = ?, group_id = ?, status = ?, outlet_id = ? WHERE id = ?");
$stmt->execute([$username, $email, $phone, $group_id, $status, $outlet_id, $id]);';
$c = str_replace($find_edit_post, $repl_edit_post, $c);
file_put_contents('index.php', $c);
echo "Edit Post Patched\n";

31
patch_users_edit_ui.php Normal file
View File

@ -0,0 +1,31 @@
<?php
$c = file_get_contents('index.php');
$find_group_edit = '<select name="group_id" class="form-select">
<option value="">--- No Group ---</option>
<?php foreach (($data[\'role_groups\'] ?? []) as $g): ?>
<option value="<?= $g[\'id\'] ?>" <?= ($u[\'group_id\'] ?? null) == $g[\'id\'] ? \'selected\' : \'\' ?>><?= htmlspecialchars((string)$g[\'name\']) ?></option>
<?php endforeach; ?>
</select>
</div>';
$repl_group_edit = $find_group_edit . '
<div class="mb-3">
<label class="form-label fw-semibold" data-en="Assigned Outlet" data-ar="الفرع">Assigned Outlet</label>
<select name="outlet_id" class="form-select">
<option value="">--- Global (Admin only) ---</option>
<?php foreach (($data[\'outlets\'] ?? []) as $o): ?>
<option value="<?= $o[\'id\'] ?>" <?= ($u[\'outlet_id\'] ?? null) == $o[\'id\'] ? \'selected\' : \'\' ?>><?= htmlspecialchars($o[\'name\']) ?></option>
<?php endforeach; ?>
</select>
</div>';
$c = str_replace($find_group_edit, $repl_group_edit, $c);
// Also we need to make sure 'status' was updated in Edit User POST!
// Looking at the previous edit_user patch, I didn't add status. I will add status now since the UI has it!
// Actually, earlier the backend code didn't update `status`?
// Let me just replace the file contents.
file_put_contents('index.php', $c);
echo "UI Edit Patched\n";

34
patch_users_ui.php Normal file
View File

@ -0,0 +1,34 @@
<?php
$c = file_get_contents('index.php');
// Fetch outlets for users
$c = str_replace(
"\$data['role_groups'] = db()->query(\"SELECT id, name FROM role_groups ORDER BY name ASC\")->fetchAll();\n break;\n case 'backups':",
"\$data['role_groups'] = db()->query(\"SELECT id, name FROM role_groups ORDER BY name ASC\")->fetchAll();\n \$data['outlets'] = db()->query(\"SELECT id, name FROM outlets ORDER BY name ASC\")->fetchAll();\n break;\n case 'backups':",
$c
);
// Add Outlet field to Add User
$find_group_add = '<select name="group_id" class="form-select">
<option value="">--- Select Group ---</option>
<?php foreach (($data[\'role_groups\'] ?? []) as $g): ?>
<option value="<?= $g[\'id\'] ?>"><?= htmlspecialchars($g[\'name\']) ?></option>
<?php endforeach; ?>
</select>
</div>';
$repl_group_add = $find_group_add . '
<div class="mb-3">
<label class="form-label" data-en="Assigned Outlet" data-ar="الفرع">Assigned Outlet</label>
<select name="outlet_id" class="form-select">
<option value="">--- Global (Admin only) ---</option>
<?php foreach (($data[\'outlets\'] ?? []) as $o): ?>
<option value="<?= $o[\'id\'] ?>"><?= htmlspecialchars($o[\'name\']) ?></option>
<?php endforeach; ?>
</select>
</div>';
$c = str_replace($find_group_add, $repl_group_add, $c);
file_put_contents('index.php', $c);
echo "UI Patched\n";

View File

@ -1 +1,11 @@
2026-02-25 09:56:38 - POST: {"action":"translate","text":"Product 2","target":"ar"}
2026-02-25 12:57:47 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":25}]","total_amount":"25","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":7,\"qty\":1,\"price\":10,\"vat_rate\":0,\"vat_amount\":0},{\"id\":8,\"qty\":1,\"price\":15,\"vat_rate\":0,\"vat_amount\":0}]"}
2026-02-25 13:23:21 - POST: {"sync_accounting":""}
2026-02-25 13:30:19 - POST: {"action":"save_theme","theme":"nord"}
2026-02-25 13:30:28 - POST: {"action":"save_theme","theme":"forest"}
2026-02-25 13:30:36 - POST: {"action":"save_theme","theme":"citrus"}
2026-02-25 13:30:39 - POST: {"action":"save_theme","theme":"forest"}
2026-02-25 13:30:41 - POST: {"action":"save_theme","theme":"nord"}
2026-02-25 13:31:58 - POST: {"id":"","name":"Nizwa Branch","phone":"9689555","address":"Nizwa","status":"active","add_outlet":""}
2026-02-25 17:08:42 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":15.45}]","total_amount":"15.45","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":1,\"qty\":1,\"price\":0.45,\"vat_rate\":0,\"vat_amount\":0},{\"id\":8,\"qty\":1,\"price\":15,\"vat_rate\":0,\"vat_amount\":0}]"}
2026-02-25 17:09:31 - POST: {"id":"37","delete_invoice":""}

View File

@ -1 +1,3 @@
2026-02-25 04:49:58 - search_items call: q=on
2026-02-25 12:57:14 - search_items call: q=on
2026-02-25 17:10:03 - search_items call: q=on