feat: Add support for users to access multiple branches/outlets

This commit is contained in:
Flatlogic Bot 2026-04-20 03:23:35 +00:00
parent 2d79f6ac5f
commit 393595fe77
12 changed files with 117 additions and 145 deletions

View File

@ -20,7 +20,7 @@ $pageTitle = tr('تعديل فاتورة', 'Edit Invoice') . ' #' . h($editSale[
$activeNav = 'sales';
$error = '';
$catalog = catalog();
$allowedBranches = $user['role'] === 'owner' ? array_keys(branches()) : [$user['branch_code']];
$allowedBranches = get_user_branches($user);
try {
$customers = db()->query('SELECT id, name, phone FROM customers ORDER BY name ASC')->fetchAll();

View File

@ -22,7 +22,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'create' && has_permission('expenses', 'add')) {
$branch_code = $isOwner ? ($_POST['branch_code'] ?? null) : $userBranch;
$pb = $_POST['branch_code'] ?? ''; $branch_code = can_access_branch($pb) ? $pb : $userBranch; if ($pb === '' && $user['role'] === 'owner') { $branch_code = null; } else if ($branch_code === '') { $branch_code = null; }
$stmt = $pdo->prepare('INSERT INTO expenses (branch_code, category_id, amount, expense_date, description, created_by) VALUES (?, ?, ?, ?, ?, ?)');
$stmt->execute([
$branch_code === '' ? null : $branch_code,
@ -35,7 +35,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
set_flash('success', tr('تمت إضافة المصروف بنجاح', 'Expense added successfully'));
redirect_to('expenses.php');
} elseif ($action === 'edit' && has_permission('expenses', 'edit')) {
$branch_code = $isOwner ? ($_POST['branch_code'] ?? null) : $userBranch;
$pb = $_POST['branch_code'] ?? ''; $branch_code = can_access_branch($pb) ? $pb : $userBranch; if ($pb === '' && $user['role'] === 'owner') { $branch_code = null; } else if ($branch_code === '') { $branch_code = null; }
$stmt = $pdo->prepare('UPDATE expenses SET branch_code = ?, category_id = ?, amount = ?, expense_date = ?, description = ? WHERE id = ?');
$stmt->execute([
$branch_code === '' ? null : $branch_code,
@ -68,9 +68,17 @@ if ($search) {
$params[] = "%$search%";
}
if (!$isOwner && $userBranch) {
$where .= ' AND (e.branch_code = ? OR e.branch_code IS NULL)';
$params[] = $userBranch;
if (!$isOwner) {
$ubranches = get_user_branches($user);
if (!empty($ubranches)) {
$inQuery = implode(',', array_fill(0, count($ubranches), '?'));
$where .= " AND (e.branch_code IN ($inQuery) OR e.branch_code IS NULL)";
foreach ($ubranches as $ub) {
$params[] = $ub;
}
} else {
$where .= " AND e.branch_code IS NULL";
}
}
$totalStmt = $pdo->prepare("SELECT COUNT(*) FROM expenses e WHERE $where");

View File

@ -201,6 +201,35 @@ function require_roles(array $roles): array
return $user;
}
function get_user_branches($user): array
{
if (!$user) return [];
if ($user['role'] === 'owner') return array_keys(branches());
$list = [$user['branch_code']];
if (!empty($user['allowed_branches'])) {
$extra = explode(',', $user['allowed_branches']);
foreach ($extra as $b) {
$b = trim($b);
if ($b) $list[] = $b;
}
}
return array_unique($list);
}
function get_user_branches_assoc($user): array
{
if (!$user) return [];
$all = branches();
$allowed = get_user_branches($user);
$res = [];
foreach ($allowed as $b) {
if (isset($all[$b])) {
$res[$b] = $all[$b];
}
}
return $res;
}
function can_access_branch(string $branchCode): bool
{
$user = current_user();
@ -212,7 +241,8 @@ function can_access_branch(string $branchCode): bool
return true;
}
return $user['branch_code'] === $branchCode;
$allowed = get_user_branches($user);
return in_array($branchCode, $allowed, true);
}
function catalog(): array
@ -337,8 +367,18 @@ function base_sales_query_filters(array &$params, ?string $mode = null, ?string
$user = current_user();
if ($user && $user['role'] !== 'owner') {
$sql .= ' AND branch_code = :viewer_branch ';
$params[':viewer_branch'] = $user['branch_code'];
$ubranches = get_user_branches($user);
if (empty($ubranches)) {
$sql .= ' AND 1=0 '; // No branches allowed
} else {
$namedParams = [];
foreach ($ubranches as $i => $ub) {
$key = ':v_branch_' . $i;
$namedParams[] = $key;
$params[$key] = $ub;
}
$sql .= ' AND branch_code IN (' . implode(', ', $namedParams) . ') ';
}
}
return $sql;

View File

@ -5,7 +5,7 @@ $pageTitle = tr('فاتورة مشتريات جديدة', 'New Purchase');
$activeNav = 'new_purchase';
$error = '';
$catalog = catalog();
$allowedBranches = $user['role'] === 'owner' ? array_keys(branches()) : [$user['branch_code']];
$allowedBranches = get_user_branches($user);
try {
$customers = $customers = [];

View File

@ -5,7 +5,7 @@ $pageTitle = $saleMode === 'normal' ? tr('إنشاء فاتورة ضريبية',
$activeNav = $saleMode === 'normal' ? 'normal' : 'pos';
$error = '';
$catalog = catalog();
$allowedBranches = $user['role'] === 'owner' ? array_keys(branches()) : [$user['branch_code']];
$allowedBranches = get_user_branches($user);
try {
$customers = db()->query('SELECT id, name, phone FROM customers ORDER BY name ASC')->fetchAll();

View File

@ -1,13 +0,0 @@
<?php
$content = file_get_contents('stock.php');
$search = "echo \"\";";
$replace = 'echo "\xEF\xBB\xBF";';
if (strpos($content, $search) !== false) {
$content = str_replace($search, $replace, $content);
file_put_contents('stock.php', $content);
echo "Replaced successfully.\n";
} else {
echo "Search string not found.\n";
}

View File

@ -1,33 +0,0 @@
<?php
$content = file_get_contents('includes/header.php');
$search = <<<HTML
<a class="list-group-item list-group-item-action <?= \$activeNav === 'pos' ? 'active' : '' ?>" href="<?= h(url_for('pos.php')) ?>">
<i class="bi bi-cart-check"></i> <?= h(tr('نقاط البيع', 'POS Sale')) ?>
</a>
<a class="list-group-item list-group-item-action <?= \$activeNav === 'normal' ? 'active' : '' ?>" href="<?= h(url_for('normal_sale.php')) ?>">
<i class="bi bi-receipt"></i> <?= h(tr('بيع عادي', 'Normal Sale')) ?>
</a>
<a class="list-group-item list-group-item-action <?= \$activeNav === 'sales' ? 'active' : '' ?>" href="<?= h(url_for('sales.php')) ?>">
<i class="bi bi-journal-text"></i> <?= h(tr('المبيعات', 'Sales')) ?>
</a>
HTML;
$replace = <<<HTML
<div class="px-3 pt-3 pb-2 text-white-50 text-uppercase small fw-bold">
<?= h(tr('المبيعات', 'Sales')) ?>
</div>
<a class="list-group-item list-group-item-action <?= \$activeNav === 'sales' ? 'active' : '' ?>" href="<?= h(url_for('sales.php')) ?>">
<i class="bi bi-journal-text"></i> <?= h(tr('قائمة الفواتير', 'Invoice list')) ?>
</a>
<a class="list-group-item list-group-item-action <?= \$activeNav === 'normal' ? 'active' : '' ?>" href="<?= h(url_for('normal_sale.php')) ?>">
<i class="bi bi-plus-circle"></i> <?= h(tr('فاتورة جديدة', 'New invoice')) ?>
</a>
<a class="list-group-item list-group-item-action <?= \$activeNav === 'pos' ? 'active' : '' ?>" href="<?= h(url_for('pos.php')) ?>">
<i class="bi bi-cart-check"></i> <?= h(tr('نقاط البيع', 'POS')) ?>
</a>
HTML;
$newContent = str_replace($search, $replace, $content);
file_put_contents('includes/header.php', $newContent);
echo "Done";

View File

@ -1,69 +0,0 @@
<?php
$content = file_get_contents('stock.php');
$search = <<<'REPLACE'
if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] === UPLOAD_ERR_OK) {
$pdo = db();
$file = fopen($_FILES['csv_file']['tmp_name'], 'r');
$bom = fread($file, 3);
if ($bom !== "") rewind($file);
$header = fgetcsv($file);
$imported = 0; $updated = 0;
$pdo->beginTransaction();
try {
$stmtInsert = $pdo->prepare("INSERT INTO items (sku, name, price, cost_price, base_stock, vat, category_id, supplier_id, unit_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmtUpdate = $pdo->prepare("UPDATE items SET name=?, price=?, cost_price=?, base_stock=?, vat=?, category_id=?, supplier_id=?, unit_id=? WHERE sku=?");
$stmtCheck = $pdo->prepare("SELECT id FROM items WHERE sku=?");
while (($row = fgetcsv($file)) !== false) {
REPLACE;
$replace = <<<'REPLACE'
if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] === UPLOAD_ERR_OK) {
$pdo = db();
$file_path = $_FILES['csv_file']['tmp_name'];
$raw_content = file_get_contents($file_path);
// Prevent ZIP / XLSX
if (str_starts_with($raw_content, 'PK')) {
header('Location: stock.php?import_error=' . urlencode('يرجى حفظ الملف بصيغة CSV وليس كملف إكسل (XLSX)'));
exit;
}
// Remove UTF-8 BOM if present
if (str_starts_with($raw_content, "\xEF\xBB\xBF")) {
$raw_content = substr($raw_content, 3);
}
// Fix encoding for Windows-1256 (common in Arabic Excel exports)
if (!mb_check_encoding($raw_content, 'UTF-8')) {
$raw_content = mb_convert_encoding($raw_content, 'UTF-8', 'Windows-1256');
}
// Determine delimiter by checking first line
$first_line = strtok($raw_content, "\r\n");
$delimiter = ',';
if ($first_line !== false && substr_count($first_line, ';') > substr_count($first_line, ',')) {
$delimiter = ';';
}
$clean_file = tmpfile();
fwrite($clean_file, $raw_content);
rewind($clean_file);
$header = fgetcsv($clean_file, 0, $delimiter);
$imported = 0; $updated = 0;
$pdo->beginTransaction();
try {
$stmtInsert = $pdo->prepare("INSERT INTO items (sku, name, price, cost_price, base_stock, vat, category_id, supplier_id, unit_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmtUpdate = $pdo->prepare("UPDATE items SET name=?, price=?, cost_price=?, base_stock=?, vat=?, category_id=?, supplier_id=?, unit_id=? WHERE sku=?");
$stmtCheck = $pdo->prepare("SELECT id FROM items WHERE sku=?");
while (($row = fgetcsv($clean_file, 0, $delimiter)) !== false) {
REPLACE;
if (strpos($content, $search) !== false) {
$content = str_replace($search, $replace, $content);
file_put_contents('stock.php', $content);
echo "Replaced successfully.\n";
} else {
echo "Search string not found.\n";
}

View File

@ -6,7 +6,7 @@ $pageTitle = tr('نقاط البيع', 'Smart POS');
$activeNav = 'pos';
$error = '';
$catalog = catalog();
$allowedBranches = $user['role'] === 'owner' ? array_keys(branches()) : [$user['branch_code']];
$allowedBranches = get_user_branches($user);
try {
$pdo = db();

View File

@ -109,13 +109,29 @@ if ($tab === 'sales') {
$params[':date_to'] = $dateTo;
if ($user['role'] !== 'owner') {
$whereConditions[] = "(e.branch_code = :ubranch OR e.branch_code IS NULL)";
$params[':ubranch'] = $user['branch_code'];
if ($branchFilter && $branchFilter === $user['branch_code']) {
$ubranches = get_user_branches($user);
if ($branchFilter && $branchFilter === 'general') {
$whereConditions[] = "e.branch_code IS NULL";
} elseif ($branchFilter && in_array($branchFilter, $ubranches, true)) {
$whereConditions[] = "e.branch_code = :branch";
$params[':branch'] = $branchFilter;
} elseif ($branchFilter && $branchFilter === 'general') {
$whereConditions[] = "e.branch_code IS NULL";
} else {
if (empty($ubranches)) {
$whereConditions[] = "e.branch_code IS NULL";
} else {
$inQuery = implode(',', array_fill(0, count($ubranches), '?'));
// We must use numbered placeholders if mixing with named placeholders?
// PDO might not like mixing ? and :name.
// Let's create named placeholders for in query.
$namedParams = [];
foreach($ubranches as $i => $ub) {
$key = ':ubranch_' . $i;
$namedParams[] = $key;
$params[$key] = $ub;
}
$inQuery = implode(', ', $namedParams);
$whereConditions[] = "(e.branch_code IN ($inQuery) OR e.branch_code IS NULL)";
}
}
} else {
if ($branchFilter) {
@ -215,7 +231,7 @@ require __DIR__ . '/includes/header.php';
<select name="branch" class="form-select">
<option value=""><?= h(tr('جميع الفروع', 'All Branches')) ?></option>
<?php
$availableBranches = $user['role'] === 'owner' ? branches() : [$user['branch_code'] => branches()[$user['branch_code']]];
$availableBranches = get_user_branches_assoc($user);
foreach ($availableBranches as $code => $b):
?>
<option value="<?= h($code) ?>" <?= $branchFilter === $code ? 'selected' : '' ?>><?= h(branch_label($code)) ?></option>
@ -357,7 +373,7 @@ require __DIR__ . '/includes/header.php';
<option value=""><?= h(tr('جميع الفروع', 'All Branches')) ?></option>
<option value="general" <?= $branchFilter === 'general' ? 'selected' : '' ?>><?= h(tr('مصروفات عامة', 'General Expenses')) ?></option>
<?php
$availableBranches = $user['role'] === 'owner' ? branches() : [$user['branch_code'] => branches()[$user['branch_code']]];
$availableBranches = get_user_branches_assoc($user);
foreach ($availableBranches as $code => $b):
?>
<option value="<?= h($code) ?>" <?= $branchFilter === $code ? 'selected' : '' ?>><?= h(branch_label($code)) ?></option>
@ -488,7 +504,7 @@ require __DIR__ . '/includes/header.php';
<select name="branch" class="form-select" onchange="this.form.submit()">
<option value=""><?= h(tr('جميع الفروع', 'All Branches')) ?></option>
<?php
$availableBranches = $user['role'] === 'owner' ? branches() : [$user['branch_code'] => branches()[$user['branch_code']]];
$availableBranches = get_user_branches_assoc($user);
foreach ($availableBranches as $code => $b):
?>
<option value="<?= h($code) ?>" <?= $branchFilter === $code ? 'selected' : '' ?>><?= h(branch_label($code)) ?></option>
@ -620,7 +636,7 @@ require __DIR__ . '/includes/header.php';
<select name="branch" class="form-select" onchange="this.form.submit()">
<option value=""><?= h(tr('جميع الفروع', 'All Branches')) ?></option>
<?php
$availableBranches = $user['role'] === 'owner' ? branches() : [$user['branch_code'] => branches()[$user['branch_code']]];
$availableBranches = get_user_branches_assoc($user);
foreach ($availableBranches as $code => $b):
?>
<option value="<?= h($code) ?>" <?= $branchFilter === $code ? 'selected' : '' ?>><?= h(branch_label($code)) ?></option>

View File

@ -40,8 +40,18 @@ try {
$params[':branch_code'] = $branch;
}
if ($user && $user['role'] !== 'owner') {
$where .= ' AND branch_code = :viewer_branch ';
$params[':viewer_branch'] = $user['branch_code'];
$ubranches = get_user_branches($user);
if (empty($ubranches)) {
$where .= ' AND 1=0 ';
} else {
$namedParams = [];
foreach ($ubranches as $i => $ub) {
$key = ':v_branch_' . $i;
$namedParams[] = $key;
$params[$key] = $ub;
}
$where .= ' AND branch_code IN (' . implode(', ', $namedParams) . ') ';
}
}
if ($search) {

View File

@ -22,9 +22,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($username && $password && $name_ar) {
$hash = password_hash($password, PASSWORD_DEFAULT);
try {
$stmt = db()->prepare("INSERT INTO users (username, password, role, branch_code, name_ar, name_en, permissions) VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt = db()->prepare("INSERT INTO users (username, password, role, branch_code, allowed_branches, name_ar, name_en, permissions) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$perms = isset($_POST["permissions"]) ? json_encode($_POST["permissions"]) : "{}";
$stmt->execute([$username, $hash, $role, $branch_code, $name_ar, $name_en, $perms]);
$allowed_branches = isset($_POST["allowed_branches"]) && is_array($_POST["allowed_branches"]) ? implode(",", $_POST["allowed_branches"]) : null;
$stmt->execute([$username, $hash, $role, $branch_code, $allowed_branches, $name_ar, $name_en, $perms]);
set_flash('success', tr('تمت إضافة المستخدم بنجاح.', 'User added successfully.'));
} catch (PDOException $e) {
set_flash('error', tr('حدث خطأ، قد يكون اسم المستخدم موجوداً مسبقاً.', 'Error occurred, username might already exist.'));
@ -49,13 +50,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
if ($password) {
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = db()->prepare("UPDATE users SET username=?, password=?, role=?, branch_code=?, name_ar=?, name_en=?, permissions=? WHERE id=?");
$stmt = db()->prepare("UPDATE users SET username=?, password=?, role=?, branch_code=?, allowed_branches=?, name_ar=?, name_en=?, permissions=? WHERE id=?");
$perms = isset($_POST["permissions"]) ? json_encode($_POST["permissions"]) : "{}";
$stmt->execute([$username, $hash, $role, $branch_code, $name_ar, $name_en, $perms, $id]);
$allowed_branches = isset($_POST["allowed_branches"]) && is_array($_POST["allowed_branches"]) ? implode(",", $_POST["allowed_branches"]) : null;
$stmt->execute([$username, $hash, $role, $branch_code, $allowed_branches, $name_ar, $name_en, $perms, $id]);
} else {
$stmt = db()->prepare("UPDATE users SET username=?, role=?, branch_code=?, name_ar=?, name_en=?, permissions=? WHERE id=?");
$stmt = db()->prepare("UPDATE users SET username=?, role=?, branch_code=?, allowed_branches=?, name_ar=?, name_en=?, permissions=? WHERE id=?");
$perms = isset($_POST["permissions"]) ? json_encode($_POST["permissions"]) : "{}";
$stmt->execute([$username, $role, $branch_code, $name_ar, $name_en, $perms, $id]);
$allowed_branches = isset($_POST["allowed_branches"]) && is_array($_POST["allowed_branches"]) ? implode(",", $_POST["allowed_branches"]) : null;
$stmt->execute([$username, $role, $branch_code, $allowed_branches, $name_ar, $name_en, $perms, $id]);
}
set_flash('success', tr('تم تعديل المستخدم بنجاح.', 'User updated successfully.'));
} catch (PDOException $e) {
@ -293,7 +296,17 @@ function openAddModal() {
document.getElementById('userAction').value = 'add';
document.getElementById('userId').value = '';
document.getElementById('userForm').reset();
document.querySelectorAll('.perm-check').forEach(cb => cb.checked = false);
document.querySelectorAll('.additional-branch').forEach(cb => cb.checked = false);
document.querySelectorAll('.additional-branch').forEach(cb => cb.checked = false);
if (account.allowed_branches) {
let abs = account.allowed_branches.split(',');
abs.forEach(b => {
let cb = document.getElementById('ab_' + b.trim());
if (cb) cb.checked = true;
});
}
document.querySelectorAll('.perm-check').forEach(cb => cb.checked = false);
document.getElementById('userModalLabel').innerText = '<?= h(tr('إضافة مستخدم جديد', 'Add New User')) ?>';
document.getElementById('password').required = true;
document.getElementById('passwordHelp').innerText = '';