Autosave: 20260318-094533

This commit is contained in:
Flatlogic Bot 2026-03-18 09:45:33 +00:00
parent 9a2273fa5b
commit a410151a7c
13 changed files with 633 additions and 286 deletions

View File

@ -149,6 +149,7 @@ body {
}
.nav-link {
font-weight: 400; /* Normal font for sub items */
color: rgba(255, 255, 255, 0.9); /* Lighter text for blue bg */
padding: 0.6rem 1rem;
display: flex;
@ -169,8 +170,8 @@ body {
.nav-section-title {
font-size: 0.85rem;
font-weight: 600;
color: rgba(255, 255, 255, 0.7) !important; /* Lighter text for blue bg */
font-weight: 700 !important; /* Bold for main category */
color: #38bdf8 !important; /* Sky Blue for distinction */
letter-spacing: 0.05em;
cursor: pointer;
display: flex;
@ -713,4 +714,4 @@ body:not(.theme-default) .form-select:focus {
.form-grid-3 .form-select,
.form-grid-3 .input-group {
width: 100%;
}
}

View File

@ -0,0 +1,2 @@
ALTER TABLE purchases ADD COLUMN outlet_id INT DEFAULT 1;
UPDATE purchases SET outlet_id = 1 WHERE outlet_id IS NULL;

View File

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS outlets (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
address TEXT NULL,
phone VARCHAR(50) NULL,
status ENUM('active', 'inactive') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO outlets (id, name, address, phone, status, created_at)
SELECT 1, 'Main Outlet', 'Head Office', '', 'active', NOW()
WHERE NOT EXISTS (SELECT 1 FROM outlets WHERE id = 1);

View File

@ -0,0 +1,65 @@
-- Multi-Outlet Implementation
-- 1. Create outlet_stock table
CREATE TABLE IF NOT EXISTS outlet_stock (
id INT AUTO_INCREMENT PRIMARY KEY,
outlet_id INT NOT NULL,
item_id INT NOT NULL,
quantity DECIMAL(15, 2) DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_item_outlet (outlet_id, item_id),
CONSTRAINT fk_outlet_stock_outlet FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE CASCADE,
CONSTRAINT fk_outlet_stock_item FOREIGN KEY (item_id) REFERENCES stock_items(id) ON DELETE CASCADE
);
-- 2. Migrate existing stock to Default Outlet (ID 1)
-- We assume Outlet 1 exists (created in previous migration)
INSERT INTO outlet_stock (outlet_id, item_id, quantity)
SELECT 1, id, stock_quantity FROM stock_items
ON DUPLICATE KEY UPDATE quantity = stock_items.stock_quantity;
-- 3. Add outlet_id to tables
-- Invoices
ALTER TABLE invoices ADD COLUMN IF NOT EXISTS outlet_id INT DEFAULT NULL;
ALTER TABLE invoices ADD CONSTRAINT fk_invoices_outlet FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE SET NULL;
UPDATE invoices SET outlet_id = 1 WHERE outlet_id IS NULL;
-- POS Transactions
ALTER TABLE pos_transactions ADD COLUMN IF NOT EXISTS outlet_id INT DEFAULT NULL;
ALTER TABLE pos_transactions ADD CONSTRAINT fk_pos_trans_outlet FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE SET NULL;
UPDATE pos_transactions SET outlet_id = 1 WHERE outlet_id IS NULL;
-- POS Held Carts
ALTER TABLE pos_held_carts ADD COLUMN IF NOT EXISTS outlet_id INT DEFAULT NULL;
ALTER TABLE pos_held_carts ADD CONSTRAINT fk_pos_carts_outlet FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE SET NULL;
UPDATE pos_held_carts SET outlet_id = 1 WHERE outlet_id IS NULL;
-- Quotations
ALTER TABLE quotations ADD COLUMN IF NOT EXISTS outlet_id INT DEFAULT NULL;
ALTER TABLE quotations ADD CONSTRAINT fk_quotations_outlet FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE SET NULL;
UPDATE quotations SET outlet_id = 1 WHERE outlet_id IS NULL;
-- LPOs
ALTER TABLE lpos ADD COLUMN IF NOT EXISTS outlet_id INT DEFAULT NULL;
-- Note: lpos might already have outlet_id from a previous failed attempt or partial migration, checking IF NOT EXISTS is good.
-- We need to check if the foreign key exists before adding it to avoid errors, or just try adding it.
-- For simplicity in this environment, we'll try to add it. If it fails, it might be due to duplicate name.
-- Let's use a safe procedure for FKs if possible, or just standard ALTER.
-- safe bet:
ALTER TABLE lpos ADD CONSTRAINT fk_lpos_outlet FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE SET NULL;
UPDATE lpos SET outlet_id = 1 WHERE outlet_id IS NULL;
-- Sales Returns
ALTER TABLE sales_returns ADD COLUMN IF NOT EXISTS outlet_id INT DEFAULT NULL;
ALTER TABLE sales_returns ADD CONSTRAINT fk_sales_returns_outlet FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE SET NULL;
UPDATE sales_returns SET outlet_id = 1 WHERE outlet_id IS NULL;
-- Purchase Returns
ALTER TABLE purchase_returns ADD COLUMN IF NOT EXISTS outlet_id INT DEFAULT NULL;
ALTER TABLE purchase_returns ADD CONSTRAINT fk_purchase_returns_outlet FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE SET NULL;
UPDATE purchase_returns SET outlet_id = 1 WHERE outlet_id IS NULL;
-- Expenses
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS outlet_id INT DEFAULT NULL;
ALTER TABLE expenses ADD CONSTRAINT fk_expenses_outlet FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE SET NULL;
UPDATE expenses SET outlet_id = 1 WHERE outlet_id IS NULL;

View File

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS user_outlets (
user_id INT(11) NOT NULL,
outlet_id INT(11) NOT NULL,
PRIMARY KEY (user_id, outlet_id),
CONSTRAINT fk_user_outlets_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_user_outlets_outlet FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE CASCADE
);

58
fix_index_py.py Normal file
View File

@ -0,0 +1,58 @@
with open('index.php', 'r') as f:
lines = f.readlines()
# The corrupted block starts around line 50 (index 50 is line 51)
# And ends around line 65 where "// Timezone Setup" is.
# Let's find "require_once 'db/config.php';" which I added, and "// Timezone Setup"
start_idx = -1
end_idx = -1
for i, line in enumerate(lines):
if "require_once 'db/config.php';" in line:
start_idx = i
if "// Timezone Setup" in line:
end_idx = i
break
if start_idx != -1 and end_idx != -1:
print(f"Replacing lines {start_idx+1} to {end_idx}")
new_block = """require_once 'db/config.php';
require_once 'includes/stock_helper.php';
// Helper for current outlet
if (!function_exists('current_outlet_id')) {
function current_outlet_id() {
if (session_status() === PHP_SESSION_NONE) session_start();
return (int)($_SESSION['outlet_id'] ?? 1);
}
}
// Handle Outlet Switch
if (isset($_GET['action']) && $_GET['action'] === 'switch_outlet' && isset($_GET['id'])) {
$target_id = (int)$_GET['id'];
$allowed_outlets = $_SESSION['user_outlets'] ?? [1];
$is_admin = ($_SESSION['user_role_name'] ?? '') === 'Administrator';
if ($is_admin || in_array($target_id, $allowed_outlets)) {
$stmt = db()->prepare("SELECT id FROM outlets WHERE id = ? AND status = 'active'");
$stmt->execute([$target_id]);
if ($stmt->fetchColumn()) {
$_SESSION['outlet_id'] = $target_id;
}
}
header("Location: index.php");
exit;
}
"""
# Replace the slice
lines[start_idx:end_idx] = [new_block]
with open('index.php', 'w') as f:
f.writelines(lines)
print("Fixed index.php")
else:
print("Could not find block boundaries.")

32
includes/stock_helper.php Normal file
View File

@ -0,0 +1,32 @@
<?php
// includes/stock_helper.php
if (!function_exists('current_outlet_id')) {
function current_outlet_id() {
if (session_status() === PHP_SESSION_NONE) session_start();
return (int)($_SESSION['outlet_id'] ?? 1);
}
}
if (!function_exists('update_stock')) {
function update_stock($item_id, $qty, $outlet_id = null) {
if ($outlet_id === null) {
$outlet_id = current_outlet_id();
}
$db = db();
// 1. Update/Insert into outlet_stock (Per-Outlet Source of Truth)
$check = $db->prepare("SELECT id FROM outlet_stock WHERE outlet_id = ? AND item_id = ?");
$check->execute([$outlet_id, $item_id]);
if (!$check->fetchColumn()) {
$db->prepare("INSERT INTO outlet_stock (outlet_id, item_id, quantity) VALUES (?, ?, 0)")->execute([$outlet_id, $item_id]);
}
$stmt = $db->prepare("UPDATE outlet_stock SET quantity = quantity + ? WHERE outlet_id = ? AND item_id = ?");
$stmt->execute([$qty, $outlet_id, $item_id]);
// 2. Update global stock_items (Legacy/Aggregate Cache)
$stmtGlobal = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?");
$stmtGlobal->execute([$qty, $item_id]);
}
}

399
index.php
View File

@ -49,6 +49,32 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
file_put_contents('post_debug.log', date('Y-m-d H:i:s') . " - POST: " . json_encode($_POST) . "\n", FILE_APPEND);
}
require_once 'db/config.php';
require_once 'includes/stock_helper.php';
// Helper for current outlet
if (!function_exists('current_outlet_id')) {
function current_outlet_id() {
if (session_status() === PHP_SESSION_NONE) session_start();
return (int)($_SESSION['outlet_id'] ?? 1);
}
}
// Handle Outlet Switch
if (isset($_GET['action']) && $_GET['action'] === 'switch_outlet' && isset($_GET['id'])) {
$target_id = (int)$_GET['id'];
$allowed_outlets = $_SESSION['user_outlets'] ?? [1];
$is_admin = ($_SESSION['user_role_name'] ?? '') === 'Administrator';
if ($target_id === -1) { $_SESSION['outlet_id'] = -1; } elseif ($is_admin || in_array($target_id, $allowed_outlets)) {
$stmt = db()->prepare("SELECT id FROM outlets WHERE id = ? AND status = 'active'");
$stmt->execute([$target_id]);
if ($stmt->fetchColumn()) {
$_SESSION['outlet_id'] = $target_id;
}
}
header("Location: index.php");
exit;
}
// Timezone Setup
try {
@ -195,7 +221,6 @@ if ($page === 'activate') {
exit;
}
require_once 'db/BackupService.php';
require_once 'includes/accounting_helper.php';
// Helper to check permissions
@ -357,6 +382,23 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
$_SESSION['profile_pic'] = $u['profile_pic'];
$_SESSION['theme'] = $u['theme'] ?? 'default';
// --- Multi-Outlet Login Logic ---
// Fetch assigned outlets
$outletStmt = db()->prepare("SELECT outlet_id FROM user_outlets WHERE user_id = ?");
$outletStmt->execute([$u['id']]);
$user_outlets = $outletStmt->fetchAll(PDO::FETCH_COLUMN);
if (empty($user_outlets)) {
if (($u['role_name'] ?? '') === 'Administrator') {
$allOutlets = db()->query("SELECT id FROM outlets WHERE status = 'active'")->fetchAll(PDO::FETCH_COLUMN);
$user_outlets = $allOutlets ?: [1];
} else {
$user_outlets = [1];
}
}
$_SESSION['user_outlets'] = $user_outlets;
$_SESSION['outlet_id'] = $user_outlets[0];
header("Location: index.php");
exit;
} else {
@ -401,7 +443,7 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
exit;
}
$searchTerm = "%$q%";
$stmt = db()->prepare("SELECT * FROM stock_items WHERE name_en LIKE ? OR name_ar LIKE ? OR sku LIKE ? LIMIT 15");
$oid = current_outlet_id(); $stmt = db()->prepare("SELECT i.*, COALESCE(os.quantity, 0) as stock_quantity FROM stock_items i LEFT JOIN outlet_stock os ON i.id = os.item_id AND os.outlet_id = $oid WHERE (i.name_en LIKE ? OR i.name_ar LIKE ? OR i.sku LIKE ?) LIMIT 15");
$stmt->execute([$searchTerm, $searchTerm, $searchTerm]);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
exit;
@ -491,19 +533,19 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
$items_for_journal[] = ['id' => $item['id'], 'qty' => $item['qty']];
}
$stmt = $db->prepare("INSERT INTO invoices (transaction_no, customer_id, invoice_date, payment_type, total_amount, vat_amount, total_with_vat, paid_amount, status, register_session_id, is_pos, discount_amount, loyalty_points_redeemed, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'paid', ?, 1, ?, ?, ?)");
$stmt = $db->prepare("INSERT INTO invoices (transaction_no, customer_id, invoice_date, payment_type, total_amount, vat_amount, total_with_vat, paid_amount, status, register_session_id, is_pos, discount_amount, loyalty_points_redeemed, created_by, outlet_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'paid', ?, 1, ?, ?, ?, ?)");
$stmt->execute([$transaction_no, $customer_id, date('Y-m-d'), 'pos', $total_amount, $tax_amount, $net_amount, $net_amount, $session_id, $discount_amount, $loyalty_redeemed, $_SESSION['user_id']]);
$transaction_id = (int)$db->lastInsertId();
// Insert Items & Update Stock
$stmtItem = $db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)");
$stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
// $stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
foreach ($items as $item) {
$sub = (float)$item['price'] * (float)$item['qty'];
$va = (float)($item['vat_amount'] ?? 0);
$stmtItem->execute([$transaction_id, $item['id'], $item['qty'], $item['price'], $va, $sub]);
$stmtStock->execute([$item['qty'], $item['id']]);
update_stock($item['id'], -$item['qty']);
}
// Insert Payments
@ -946,7 +988,7 @@ function getPromotionalPrice($item) {
// Update stock
$change = ($type === 'sale') ? -$qty : $qty;
$db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?")->execute([$change, $item_id]);
update_stock($item_id, $change);
$items_for_journal[] = ['id' => $item_id, 'qty' => $qty];
}
@ -1120,7 +1162,7 @@ function getPromotionalPrice($item) {
$total_with_vat = $total_subtotal + $total_vat;
$stmt = $db->prepare("INSERT INTO lpos (supplier_id, lpo_date, delivery_date, status, total_amount, vat_amount, total_with_vat, terms_conditions) VALUES (?, ?, ?, 'pending', ?, ?, ?, ?)");
$stmt = $db->prepare("INSERT INTO lpos (supplier_id, lpo_date, delivery_date, status, total_amount, vat_amount, total_with_vat, terms_conditions, outlet_id) VALUES (?, ?, ?, 'pending', ?, ?, ?, ?, ?)");
$stmt->execute([$supp_id, $lpo_date, $delivery_date, $total_subtotal, $total_vat, $total_with_vat, $terms]);
$lpo_id = $db->lastInsertId();
@ -1225,8 +1267,8 @@ function getPromotionalPrice($item) {
// Create Invoice
$inv_date = date('Y-m-d');
$stmtInv = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0)");
$stmtInv->execute([$quot['customer_id'], $inv_date, $quot['total_amount'], $quot['vat_amount'], $quot['total_with_vat']]);
$stmtInv = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount, outlet_id) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0, ?)");
$stmtInv->execute([$quot['customer_id'], $inv_date, $quot['total_amount'], $quot['vat_amount'], $quot['total_with_vat'], current_outlet_id()]);
$inv_id = $db->lastInsertId();
$items_for_journal = [];
@ -1234,7 +1276,7 @@ function getPromotionalPrice($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']]);
update_stock($item['item_id'], -$item['quantity']);
$items_for_journal[] = ['id' => $item['item_id'], 'qty' => $item['quantity']];
}
@ -1268,8 +1310,8 @@ function getPromotionalPrice($item) {
// Create Purchase Invoice
$pur_date = date('Y-m-d');
$stmtPur = $db->prepare("INSERT INTO purchases (supplier_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0)");
$stmtPur->execute([$lpo['supplier_id'], $pur_date, $lpo['total_amount'], $lpo['vat_amount'], $lpo['total_with_vat']]);
$stmtPur = $db->prepare("INSERT INTO purchases (supplier_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount, outlet_id) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0, ?)");
$stmtPur->execute([$lpo['supplier_id'], $pur_date, $lpo['total_amount'], $lpo['vat_amount'], $lpo['total_with_vat'], current_outlet_id()]);
$pur_id = $db->lastInsertId();
$items_for_journal = [];
@ -1277,7 +1319,7 @@ function getPromotionalPrice($item) {
$db->prepare("INSERT INTO purchase_items (purchase_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$pur_id, $item['item_id'], $item['quantity'], $item['unit_price'], $item['vat_amount'], $item['total_amount']]);
// Update stock
$db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?")->execute([$item['quantity'], $item['item_id']]);
update_stock($item['item_id'], $item['quantity']);
$items_for_journal[] = ['id' => $item['item_id'], 'qty' => $item['quantity']];
}
@ -1461,7 +1503,7 @@ function getPromotionalPrice($item) {
$total_with_vat = $total_subtotal + $total_vat;
$stmt = $db->prepare("INSERT INTO lpos (supplier_id, lpo_date, delivery_date, status, total_amount, vat_amount, total_with_vat, terms_conditions) VALUES (?, ?, ?, 'pending', ?, ?, ?, ?)");
$stmt = $db->prepare("INSERT INTO lpos (supplier_id, lpo_date, delivery_date, status, total_amount, vat_amount, total_with_vat, terms_conditions, outlet_id) VALUES (?, ?, ?, 'pending', ?, ?, ?, ?, ?)");
$stmt->execute([$supp_id, $lpo_date, $delivery_date, $total_subtotal, $total_vat, $total_with_vat, $terms]);
$lpo_id = $db->lastInsertId();
@ -1569,8 +1611,8 @@ function getPromotionalPrice($item) {
// Create Invoice
$inv_date = date('Y-m-d');
$stmtInv = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0)");
$stmtInv->execute([$quot['customer_id'], $inv_date, $quot['total_amount'], $quot['vat_amount'], $quot['total_with_vat']]);
$stmtInv = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount, outlet_id) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0, ?)");
$stmtInv->execute([$quot['customer_id'], $inv_date, $quot['total_amount'], $quot['vat_amount'], $quot['total_with_vat'], current_outlet_id()]);
$inv_id = $db->lastInsertId();
$items_for_journal = [];
@ -1578,7 +1620,7 @@ function getPromotionalPrice($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']]);
update_stock($item['item_id'], -$item['quantity']);
$items_for_journal[] = ['id' => $item['item_id'], 'qty' => $item['quantity']];
}
@ -1612,8 +1654,8 @@ function getPromotionalPrice($item) {
// Create Purchase Invoice
$inv_date = date('Y-m-d');
$stmtPur = $db->prepare("INSERT INTO purchases (supplier_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0)");
$stmtPur->execute([$lpo['supplier_id'], $inv_date, $lpo['total_amount'], $lpo['vat_amount'], $lpo['total_with_vat']]);
$stmtPur = $db->prepare("INSERT INTO purchases (supplier_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount, outlet_id) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0, ?)");
$stmtPur->execute([$lpo['supplier_id'], $inv_date, $lpo['total_amount'], $lpo['vat_amount'], $lpo['total_with_vat'], current_outlet_id()]);
$pur_id = $db->lastInsertId();
$items_for_journal = [];
@ -1621,7 +1663,7 @@ function getPromotionalPrice($item) {
$db->prepare("INSERT INTO purchase_items (purchase_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$pur_id, $item['item_id'], $item['quantity'], $item['unit_price'], $item['vat_amount'], $item['total_amount']]);
// Update stock
$db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?")->execute([$item['quantity'], $item['item_id']]);
update_stock($item['item_id'], $item['quantity']);
$items_for_journal[] = ['id' => $item['item_id'], 'qty' => $item['quantity']];
}
@ -1840,7 +1882,7 @@ function getPromotionalPrice($item) {
$oldItems = $stmtOld->fetchAll();
foreach ($oldItems as $old) {
$change = ($type === 'sale') ? (float)$old['quantity'] : -(float)$old['quantity'];
$db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?")->execute([$change, $old['item_id']]);
update_stock($old['item_id'], $change);
}
// Delete old items
@ -1861,7 +1903,7 @@ function getPromotionalPrice($item) {
$db->prepare("INSERT INTO $item_table ($fk_col, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$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]);
update_stock($item_id, $change);
}
$db->commit();
@ -2056,7 +2098,7 @@ if (isset($_POST['add_hr_department'])) {
// Insert Return Items and Update Stock
$stmtItem = $db->prepare("INSERT INTO sales_return_items (return_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
$stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?");
// $stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?");
foreach ($item_ids as $i => $item_id) {
$qty = (float)$quantities[$i];
@ -2064,7 +2106,7 @@ if (isset($_POST['add_hr_department'])) {
$price = (float)$prices[$i];
$line_total = $qty * $price;
$stmtItem->execute([$return_id, $item_id, $qty, $price, $line_total]);
$stmtStock->execute([$qty, $item_id]);
update_stock($item_id, $qty);
}
}
@ -2107,7 +2149,7 @@ if (isset($_POST['add_hr_department'])) {
// Insert Return Items and Update Stock
$stmtItem = $db->prepare("INSERT INTO purchase_return_items (return_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
$stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
// $stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
foreach ($item_ids as $i => $item_id) {
$qty = (float)$quantities[$i];
@ -2115,7 +2157,7 @@ if (isset($_POST['add_hr_department'])) {
$price = (float)$prices[$i];
$line_total = $qty * $price;
$stmtItem->execute([$return_id, $item_id, $qty, $price, $line_total]);
$stmtStock->execute([$qty, $item_id]);
update_stock($item_id, -$qty);
}
}
@ -2250,28 +2292,7 @@ if (isset($_POST['add_hr_department'])) {
}
}
if (isset($_POST['add_user'])) {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$email = $_POST['email'] ?? '';
$phone = $_POST['phone'] ?? '';
$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 (?, ?, ?, ?, ?)");
try {
$stmt->execute([$username, $hashed_password, $email, $phone, $group_id]);
$message = "User added successfully!";
} catch (PDOException $e) {
if ($e->getCode() == '23000') {
$message = "Error: Username already exists.";
} else {
$message = "Error adding user: " . $e->getMessage();
}
}
}
}
if (isset($_POST['edit_role_group'])) {
if (isset($_POST['edit_role_group'])) {
$id = (int)$_POST['id'];
$name = $_POST['name'] ?? '';
$permissions = isset($_POST['permissions']) ? $_POST['permissions'] : [];
@ -2308,34 +2329,7 @@ if (isset($_POST['add_hr_department'])) {
$message = "Role Group deleted successfully!";
}
}
if (isset($_POST['edit_user'])) {
$id = (int)$_POST['id'];
$username = $_POST['username'] ?? '';
$email = $_POST['email'] ?? '';
$phone = $_POST['phone'] ?? '';
$group_id = (int)($_POST['group_id'] ?? 0) ?: null;
$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]);
if (!empty($_POST['password'])) {
$hashed_password = password_hash($_POST['password'], PASSWORD_DEFAULT);
$stmt = db()->prepare("UPDATE users SET password = ? WHERE id = ?");
$stmt->execute([$hashed_password, $id]);
}
$message = "User updated successfully!";
}
}
if (isset($_POST['delete_user'])) {
$id = (int)$_POST['id'];
if ($id) {
$stmt = db()->prepare("DELETE FROM users WHERE id = ?");
$stmt->execute([$id]);
$message = "User deleted successfully!";
}
}
// --- POS Devices Handlers ---
if (isset($_POST['add_pos_device'])) {
$name = $_POST['device_name'] ?? '';
@ -3332,7 +3326,7 @@ switch ($page) {
}
unset($inv);
$items_list_raw = db()->query("SELECT id, name_en, name_ar, sale_price, purchase_price, stock_quantity, vat_rate, is_promotion, promotion_start, promotion_end, promotion_percent FROM stock_items ORDER BY name_en ASC")->fetchAll(PDO::FETCH_ASSOC);
$oid = current_outlet_id(); $items_list_raw = db()->query("SELECT i.id, i.name_en, i.name_ar, i.sale_price, i.purchase_price, COALESCE(os.quantity, 0) as stock_quantity, i.vat_rate, i.is_promotion, i.promotion_start, i.promotion_end, i.promotion_percent FROM stock_items i LEFT JOIN outlet_stock os ON i.id = os.item_id AND os.outlet_id = $oid ORDER BY i.name_en ASC")->fetchAll(PDO::FETCH_ASSOC);
foreach ($items_list_raw as &$item) {
$item['sale_price'] = getPromotionalPrice($item);
}
@ -3479,8 +3473,7 @@ switch ($page) {
$data['role_groups'] = db()->query("SELECT * FROM role_groups ORDER BY name ASC")->fetchAll();
break;
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();
require 'pages/users_logic.php';
break;
case 'backups':
$data['backups'] = BackupService::getBackups();
@ -3679,16 +3672,31 @@ switch ($page) {
default:
if (can('dashboard_view')) {
$data['customers'] = db()->query("SELECT * FROM customers ORDER BY id DESC LIMIT 5")->fetchAll();
// Statistics with Outlet Filter
$current_oid = current_outlet_id();
$inv_cond = ($current_oid > 0) ? " WHERE outlet_id = $current_oid " : "";
$pos_cond = " WHERE status = 'completed' " . (($current_oid > 0) ? " AND outlet_id = $current_oid " : "");
$pay_inv_cond = ($current_oid > 0) ? " WHERE i.outlet_id = $current_oid " : "";
$pay_pos_cond = ($current_oid > 0) ? " WHERE t.outlet_id = $current_oid " : "";
$pur_cond = ($current_oid > 0) ? " WHERE outlet_id = $current_oid " : "";
$pay_pur_cond = ($current_oid > 0) ? " WHERE p.outlet_id = $current_oid " : "";
$low_stock_query = ($current_oid > 0)
? "SELECT COUNT(*) FROM stock_items i LEFT JOIN outlet_stock os ON i.id = os.item_id AND os.outlet_id = $current_oid WHERE COALESCE(os.quantity, 0) <= i.min_stock_level"
: "SELECT COUNT(*) FROM stock_items WHERE stock_quantity <= min_stock_level";
$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,
'total_sales' => (db()->query("SELECT SUM(total_with_vat) FROM invoices $inv_cond")->fetchColumn() ?: 0) + (db()->query("SELECT SUM(net_amount) FROM pos_transactions $pos_cond")->fetchColumn() ?: 0),
'total_received' => (db()->query("SELECT SUM(p.amount) FROM payments p JOIN invoices i ON p.invoice_id = i.id $pay_inv_cond")->fetchColumn() ?: 0) + (db()->query("SELECT SUM(pp.amount) FROM pos_payments pp JOIN pos_transactions t ON pp.transaction_id = t.id $pay_pos_cond")->fetchColumn() ?: 0),
'total_purchases' => db()->query("SELECT SUM(total_with_vat) FROM purchases $pur_cond")->fetchColumn() ?: 0,
'total_paid' => db()->query("SELECT SUM(pp.amount) FROM purchase_payments pp JOIN purchases p ON pp.purchase_id = p.id $pay_pur_cond")->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(),
'low_stock_items_count' => db()->query($low_stock_query)->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'];
@ -4211,7 +4219,41 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
<div class="text-muted" style="font-size: 0.7rem;"><?= htmlspecialchars((string)($_SESSION['user_role_name'] ?? '')) ?></div>
</div>
<div class="dropdown">
<a href="index.php?page=my_profile" class="btn btn-light rounded-circle p-0 overflow-hidden shadow-sm d-inline-block position-relative" style="width: 40px; height: 40px;" title="<?= __('edit') ?>">
<!-- Outlet Switcher -->
<?php
$user_outlets_list = $_SESSION['user_outlets'] ?? [1];
$is_admin = ($_SESSION['user_role_name'] ?? '') === 'Administrator';
if (count($user_outlets_list) > 1 || $is_admin):
$current_oid = current_outlet_id();
$current_oname = $current_oid === -1 ? (__('All Outlets') ?: 'All Outlets') : (db()->query("SELECT name FROM outlets WHERE id = $current_oid")->fetchColumn() ?: 'Outlet ' . $current_oid);
?>
<div class="dropdown d-inline-block me-3">
<button class="btn btn-outline-secondary dropdown-toggle btn-sm" type="button" data-bs-toggle="dropdown">
<i class="fas fa-store me-1"></i> <?= htmlspecialchars($current_oname) ?>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item <?= $current_oid == -1 ? 'active' : '' ?>" href="index.php?action=switch_outlet&id=-1"><?= __('All Outlets') ?: 'All Outlets' ?></a></li>
<li><hr class="dropdown-divider"></li>
<?php
$availOutlets = $user_outlets_list;
if ($is_admin) {
$availOutlets = db()->query("SELECT id FROM outlets WHERE status='active'")->fetchAll(PDO::FETCH_COLUMN);
}
foreach($availOutlets as $oid):
$oname = db()->query("SELECT name FROM outlets WHERE id=$oid")->fetchColumn();
?>
<li>
<a class="dropdown-item <?= $oid == $current_oid ? 'active' : '' ?>" href="index.php?action=switch_outlet&id=<?= $oid ?>">
<?= htmlspecialchars($oname) ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<a href="index.php?page=my_profile" class="btn btn-light rounded-circle p-0 overflow-hidden shadow-sm d-inline-block position-relative" style="width: 40px; height: 40px;" title="<?= __('edit') ?>">
<?php if (!empty($_SESSION['profile_pic'])): ?>
<img src="<?= htmlspecialchars($_SESSION['profile_pic']) ?>?v=<?= time() ?>" alt="Profile" style="width: 100%; height: 100%; object-fit: cover;">
<?php else: ?>
@ -8534,7 +8576,8 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
</div>
</div>
<?php require "outlets_html.php"; ?>
<?php elseif ($page === "outlets" && ($_SESSION["user_role_name"] ?? "") === "Administrator"): ?>
<?php require "outlets_html.php"; ?>
<?php elseif ($page === 'settings'): ?>
<div class="card p-4">
<h5 class="mb-4" data-en="Company Profile" data-ar="ملف الشركة">Company Profile</h5>
@ -8984,145 +9027,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
</div>
<?php elseif ($page === 'users'): ?>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center border-0">
<div>
<h5 class="m-0 fw-bold text-primary" data-en="User Management" data-ar="إدارة المستخدمين">User Management</h5>
<p class="text-muted small mb-0" data-en="Maintain your team accounts and security" data-ar="صيانة حسابات فريقك وأمنها">Maintain your team accounts and security</p>
</div>
<?php if (can('users_add')): ?>
<button class="btn btn-primary rounded-pill px-4" data-bs-toggle="modal" data-bs-target="#addUserModal">
<i class="bi bi-person-plus me-1"></i> <span data-en="Invite User" data-ar="دعوة مستخدم">Invite User</span>
</button>
<?php endif; ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<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="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>
</tr>
</thead>
<tbody>
<?php foreach ($data['users'] as $u): ?>
<tr>
<td class="ps-4">
<div class="d-flex align-items-center">
<?php if (!empty($u['profile_pic'])): ?>
<img src="<?= htmlspecialchars((string)$u['profile_pic']) ?>?v=<?= time() ?>" alt="Avatar" class="rounded-circle me-3 shadow-sm" style="width: 35px; height: 35px; object-fit: cover;">
<?php else: ?>
<div class="avatar-sm bg-gradient-primary rounded-circle me-3 text-white d-flex align-items-center justify-content-center fw-bold" style="width: 35px; height: 35px; background: linear-gradient(135deg, #6e8efb, #a777e3);">
<?= strtoupper(substr((string)$u['username'], 0, 1)) ?>
</div>
<?php endif; ?>
<div>
<div class="fw-bold text-dark"><?= htmlspecialchars((string)$u['username']) ?></div>
<div class="text-muted small">ID: #<?= str_pad((string)$u['id'], 4, '0', STR_PAD_LEFT) ?></div>
</div>
</div>
</td>
<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>
<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>
</td>
<td>
<?php if ($u['status'] === 'active'): ?>
<span class="badge rounded-pill bg-success bg-opacity-10 text-success px-3">Active</span>
<?php else: ?>
<span class="badge rounded-pill bg-secondary bg-opacity-10 text-secondary px-3">Suspended</span>
<?php endif; ?>
</td>
<td class="text-end pe-4">
<div class="dropdown">
<button class="btn btn-light btn-sm rounded-circle" type="button" data-bs-toggle="dropdown">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0">
<?php if (can('users_edit')): ?>
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#editUserModal<?= $u['id'] ?>"><i class="bi bi-pencil me-2 text-primary"></i> Edit Profile</a></li>
<?php endif; ?>
<?php if (can('users_delete')): ?>
<li><hr class="dropdown-divider"></li>
<li>
<form method="POST" onsubmit="return confirm('Deactivate this user account?')">
<input type="hidden" name="id" value="<?= $u['id'] ?>">
<button type="submit" name="delete_user" class="dropdown-item text-danger"><i class="bi bi-trash me-2"></i> Remove Access</button>
</form>
</li>
<?php endif; ?>
</ul>
</div>
<!-- Edit User Modal -->
<div class="modal fade" id="editUserModal<?= $u['id'] ?>" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow text-start">
<div class="modal-header">
<h5 class="modal-title fw-bold" data-en="Edit User Account" data-ar="تعديل حساب المستخدم">Edit User Account</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<input type="hidden" name="id" value="<?= $u['id'] ?>">
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-semibold" data-en="Username" data-ar="اسم المستخدم">Username</label>
<input type="text" name="username" class="form-control" value="<?= htmlspecialchars((string)$u['username']) ?>" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold" data-en="Email Address" data-ar="البريد الإلكتروني">Email Address</label>
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars((string)($u['email'] ?? '')) ?>">
</div>
<div class="mb-3">
<label class="form-label fw-semibold" data-en="Phone Number" data-ar="رقم الهاتف">Phone Number</label>
<input type="text" name="phone" class="form-control" value="<?= htmlspecialchars((string)($u['phone'] ?? '')) ?>">
</div>
<div class="mb-3">
<label class="form-label fw-semibold" data-en="Assign Role Group" data-ar="تعيين مجموعة الأدوار">Assign Role Group</label>
<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>
<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">
<option value="active" <?= $u['status'] === 'active' ? 'selected' : '' ?>>Active</option>
<option value="inactive" <?= $u['status'] === 'inactive' ? 'selected' : '' ?>>Inactive</option>
</select>
</div>
<div class="mb-3">
<label class="form-label fw-semibold" data-en="New Password" data-ar="كلمة مرور جديدة">New Password</label>
<input type="password" name="password" class="form-control" placeholder="Leave blank to keep current" autocomplete="new-password">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light rounded-pill px-3" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
<button type="submit" name="edit_user" class="btn btn-primary rounded-pill px-4" data-en="Save Changes" data-ar="حفظ التغييرات">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
</div>
<?php require 'pages/users_view.php'; ?>
<?php elseif ($page === 'cash_registers'): ?>
<div class="card p-4 rounded-4 shadow-sm border-0">
<div class="d-flex justify-content-between align-items-center mb-4">
@ -10008,49 +9913,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
</div>
<!-- Add User Modal -->
<div class="modal fade" id="addUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow">
<div class="modal-header">
<h5 class="modal-title" data-en="Add New User" data-ar="إضافة مستخدم جديد">Add New User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body">
<div class="mb-3">
<label class="form-label" data-en="Username" data-ar="اسم المستخدم">Username</label>
<input type="text" name="username" class="form-control" required autocomplete="username">
</div>
<div class="mb-3">
<label class="form-label" data-en="Email" data-ar="البريد الإلكتروني">Email</label>
<input type="email" name="email" class="form-control" autocomplete="email">
</div>
<div class="mb-3">
<label class="form-label" data-en="Phone" data-ar="الهاتف">Phone</label>
<input type="text" name="phone" class="form-control" autocomplete="tel">
</div>
<div class="mb-3">
<label class="form-label" data-en="Password" data-ar="كلمة المرور">Password</label>
<input type="password" name="password" class="form-control" required autocomplete="new-password">
</div>
<div class="mb-3">
<label class="form-label" data-en="Role Group" data-ar="مجموعة الأدوار">Role Group</label>
<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>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
<button type="submit" name="add_user" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
</div>
</form>
</div>
</div>
</div>
<!-- Add Customer Modal -->
<div class="modal fade" id="addCustomerModal" tabindex="-1">

View File

@ -1,4 +1,3 @@
<?php elseif ($page === 'outlets' && ($_SESSION['user_role_name'] ?? '') === 'Administrator'): ?>
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-header bg-white border-bottom-0 pt-4 pb-0 px-4 d-flex justify-content-between align-items-center">
<h5 class="fw-bold mb-0"><i class="bi bi-shop text-primary me-2"></i> Manage Outlets</h5>

106
pages/users_logic.php Normal file
View File

@ -0,0 +1,106 @@
<?php
// pages/users_logic.php
// Handle Actions
if (isset($_POST['add_user'])) {
if (can('users_add')) {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$email = $_POST['email'] ?? '';
$phone = $_POST['phone'] ?? '';
$group_id = (int)($_POST['group_id'] ?? 0) ?: null;
$outlet_ids = $_POST['outlet_ids'] ?? [];
if ($username && $password) {
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$stmt = db()->prepare("INSERT INTO users (username, password, email, phone, group_id) VALUES (?, ?, ?, ?, ?)");
try {
$stmt->execute([$username, $hashed_password, $email, $phone, $group_id]);
$user_id = db()->lastInsertId();
if (!empty($outlet_ids)) {
$stmtOut = db()->prepare("INSERT INTO user_outlets (user_id, outlet_id) VALUES (?, ?)");
foreach ($outlet_ids as $oid) {
$stmtOut->execute([$user_id, $oid]);
}
}
$message = "User added successfully!";
} catch (PDOException $e) {
if ($e->getCode() == '23000') {
$message = "Error: Username already exists.";
} else {
$message = "Error adding user: " . $e->getMessage();
}
}
}
}
}
if (isset($_POST['edit_user'])) {
if (can('users_edit')) {
$id = (int)$_POST['id'];
$username = $_POST['username'] ?? '';
$email = $_POST['email'] ?? '';
$phone = $_POST['phone'] ?? '';
$group_id = (int)($_POST['group_id'] ?? 0) ?: null;
$status = $_POST['status'] ?? 'active';
$outlet_ids = $_POST['outlet_ids'] ?? [];
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]);
if (!empty($_POST['password'])) {
$hashed_password = password_hash($_POST['password'], PASSWORD_DEFAULT);
$stmt = db()->prepare("UPDATE users SET password = ? WHERE id = ?");
$stmt->execute([$hashed_password, $id]);
}
// Update Outlets
db()->prepare("DELETE FROM user_outlets WHERE user_id = ?")->execute([$id]);
if (!empty($outlet_ids)) {
$stmtOut = db()->prepare("INSERT INTO user_outlets (user_id, outlet_id) VALUES (?, ?)");
foreach ($outlet_ids as $oid) {
$stmtOut->execute([$id, $oid]);
}
}
$message = "User updated successfully!";
}
}
}
if (isset($_POST['delete_user'])) {
if (can('users_delete')) {
$id = (int)$_POST['id'];
if ($id) {
$stmt = db()->prepare("DELETE FROM users WHERE id = ?");
$stmt->execute([$id]);
$message = "User deleted successfully!";
}
}
}
// Fetch Data
$page_num = isset($_GET['p']) ? max(1, (int)$_GET['p']) : 1;
$items_per_page = 20;
$offset = ($page_num - 1) * $items_per_page;
$total_users = db()->query("SELECT COUNT(*) FROM users")->fetchColumn();
$total_pages = ceil($total_users / $items_per_page);
$data['users'] = db()->query("
SELECT u.*, g.name as group_name, GROUP_CONCAT(uo.outlet_id) as outlet_ids
FROM users u
LEFT JOIN role_groups g ON u.group_id = g.id
LEFT JOIN user_outlets uo ON u.id = uo.user_id
GROUP BY u.id
ORDER BY u.username ASC
LIMIT $items_per_page OFFSET $offset
")->fetchAll();
$data['role_groups'] = db()->query("SELECT id, name FROM role_groups ORDER BY name ASC")->fetchAll();
$data['outlets'] = db()->query("SELECT * FROM outlets ORDER BY name ASC")->fetchAll();
$data['current_page'] = $page_num;
$data['total_pages'] = $total_pages;

201
pages/users_view.php Normal file
View File

@ -0,0 +1,201 @@
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center border-0">
<div>
<h5 class="m-0 fw-bold text-primary" data-en="User Management" data-ar="إدارة المستخدمين">User Management</h5>
<p class="text-muted small mb-0" data-en="Maintain your team accounts and security" data-ar="صيانة حسابات فريقك وأمنها">Maintain your team accounts and security</p>
</div>
<?php if (can('users_add')): ?>
<button class="btn btn-primary rounded-pill px-4" data-bs-toggle="modal" data-bs-target="#addUserModal">
<i class="bi bi-person-plus me-1"></i> <span data-en="Invite User" data-ar="دعوة مستخدم">Invite User</span>
</button>
<?php endif; ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<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="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>
</tr>
</thead>
<tbody>
<?php foreach ($data['users'] as $u): ?>
<tr>
<td class="ps-4">
<div class="d-flex align-items-center">
<?php if (!empty($u['profile_pic'])): ?>
<img src="<?= htmlspecialchars((string)$u['profile_pic']) ?>?v=<?= time() ?>" alt="Avatar" class="rounded-circle me-3 shadow-sm" style="width: 35px; height: 35px; object-fit: cover;">
<?php else: ?>
<div class="avatar-sm bg-gradient-primary rounded-circle me-3 text-white d-flex align-items-center justify-content-center fw-bold" style="width: 35px; height: 35px; background: linear-gradient(135deg, #6e8efb, #a777e3);">
<?= strtoupper(substr((string)$u['username'], 0, 1)) ?>
</div>
<?php endif; ?>
<div>
<div class="fw-bold text-dark"><?= htmlspecialchars((string)$u['username']) ?></div>
<div class="text-muted small">ID: #<?= str_pad((string)$u['id'], 4, '0', STR_PAD_LEFT) ?></div>
</div>
</div>
</td>
<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>
<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>
</td>
<td>
<?php if ($u['status'] === 'active'): ?>
<span class="badge rounded-pill bg-success bg-opacity-10 text-success px-3">Active</span>
<?php else: ?>
<span class="badge rounded-pill bg-secondary bg-opacity-10 text-secondary px-3">Suspended</span>
<?php endif; ?>
</td>
<td class="text-end pe-4">
<?php if (can('users_edit')): ?>
<button class="btn btn-sm btn-light text-primary rounded-circle" data-bs-toggle="modal" data-bs-target="#editUserModal<?= $u['id'] ?>" title="Edit">
<i class="bi bi-pencil"></i>
</button>
<?php endif; ?>
<?php if (can('users_delete')): ?>
<form method="POST" class="d-inline" onsubmit="return confirm('Deactivate this user account?')">
<input type="hidden" name="id" value="<?= $u['id'] ?>">
<button type="submit" name="delete_user" class="btn btn-sm btn-light text-danger rounded-circle" title="Delete">
<i class="bi bi-trash"></i>
</button>
</form>
<?php endif; ?>
<!-- Edit User Modal -->
<div class="modal fade" id="editUserModal<?= $u['id'] ?>" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow text-start">
<div class="modal-header">
<h5 class="modal-title fw-bold" data-en="Edit User Account" data-ar="تعديل حساب المستخدم">Edit User Account</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<input type="hidden" name="id" value="<?= $u['id'] ?>">
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-semibold" data-en="Username" data-ar="اسم المستخدم">Username</label>
<input type="text" name="username" class="form-control" value="<?= htmlspecialchars((string)$u['username']) ?>" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold" data-en="Email Address" data-ar="البريد الإلكتروني">Email Address</label>
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars((string)($u['email'] ?? '')) ?>">
</div>
<div class="mb-3">
<label class="form-label fw-semibold" data-en="Phone Number" data-ar="رقم الهاتف">Phone Number</label>
<input type="text" name="phone" class="form-control" value="<?= htmlspecialchars((string)($u['phone'] ?? '')) ?>">
</div>
<div class="mb-3">
<label class="form-label fw-semibold" data-en="Assign Role Group" data-ar="تعيين مجموعة الأدوار">Assign Role Group</label>
<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>
<div class="mb-3">
<label class="form-label fw-semibold" data-en="Assigned Outlets" data-ar="المنافذ المعينة">Assigned Outlets</label>
<select name="outlet_ids[]" class="form-select" multiple size="3">
<?php
$u_outlets = explode(',', $u['outlet_ids'] ?? '');
foreach ($data['outlets'] as $out):
?>
<option value="<?= $out['id'] ?>" <?= in_array($out['id'], $u_outlets) ? 'selected' : '' ?>>
<?= htmlspecialchars($out['name']) ?>
</option>
<?php endforeach; ?>
</select>
<div class="form-text small" data-en="Hold Ctrl/Cmd to select multiple" data-ar="اضغط Ctrl/Cmd لتحديد متعدد">Hold Ctrl/Cmd to select multiple.</div>
</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">
<option value="active" <?= $u['status'] === 'active' ? 'selected' : '' ?>>Active</option>
<option value="inactive" <?= $u['status'] === 'inactive' ? 'selected' : '' ?>>Inactive</option>
</select>
</div>
<div class="mb-3">
<label class="form-label fw-semibold" data-en="New Password" data-ar="كلمة مرور جديدة">New Password</label>
<input type="password" name="password" class="form-control" placeholder="Leave blank to keep current" autocomplete="new-password">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light rounded-pill px-3" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
<button type="submit" name="edit_user" class="btn btn-primary rounded-pill px-4" data-en="Save Changes" data-ar="حفظ التغييرات">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
</div>
<!-- Add User Modal -->
<div class="modal fade" id="addUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow">
<div class="modal-header">
<h5 class="modal-title" data-en="Add New User" data-ar="إضافة مستخدم جديد">Add New User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body">
<div class="mb-3">
<label class="form-label" data-en="Username" data-ar="اسم المستخدم">Username</label>
<input type="text" name="username" class="form-control" required autocomplete="username">
</div>
<div class="mb-3">
<label class="form-label" data-en="Email" data-ar="البريد الإلكتروني">Email</label>
<input type="email" name="email" class="form-control" autocomplete="email">
</div>
<div class="mb-3">
<label class="form-label" data-en="Phone" data-ar="الهاتف">Phone</label>
<input type="text" name="phone" class="form-control" autocomplete="tel">
</div>
<div class="mb-3">
<label class="form-label" data-en="Password" data-ar="كلمة المرور">Password</label>
<input type="password" name="password" class="form-control" required autocomplete="new-password">
</div>
<div class="mb-3">
<label class="form-label" data-en="Role Group" data-ar="مجموعة الأدوار">Role Group</label>
<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>
<div class="mb-3">
<label class="form-label" data-en="Assigned Outlets" data-ar="المنافذ المعينة">Assigned Outlets</label>
<select name="outlet_ids[]" class="form-select" multiple size="3">
<?php foreach ($data['outlets'] as $out): ?>
<option value="<?= $out['id'] ?>"><?= htmlspecialchars($out['name']) ?></option>
<?php endforeach; ?>
</select>
<div class="form-text small" data-en="Hold Ctrl/Cmd to select multiple" data-ar="اضغط Ctrl/Cmd لتحديد متعدد">Hold Ctrl/Cmd to select multiple.</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
<button type="submit" name="add_user" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -67,3 +67,4 @@
2026-03-18 06:31:00 - POST: {"type":"sale","customer_id":"7","invoice_date":"2026-03-18","due_date":"","payment_type":"cash","status":"unpaid","paid_amount":"0.000","item_ids":["7"],"quantities":["1"],"prices":["0.250"],"add_invoice":""}
2026-03-18 06:42:39 - POST: {"type":"sale","customer_id":"7","invoice_date":"2026-03-18","due_date":"","payment_type":"cash","status":"unpaid","paid_amount":"0.000","item_ids":["7"],"quantities":["1"],"prices":["0.250"],"add_invoice":""}
2026-03-18 06:43:05 - POST: {"invoice_id":"31","customer_id":"7","invoice_date":"2026-03-18","due_date":"","payment_type":"cash","status":"paid","paid_amount":"0.000","item_ids":["7"],"quantities":["1.00"],"prices":["0.250"],"edit_invoice":""}
2026-03-18 07:19:13 - POST: {"id":"","name":"Nizwa Outlet","phone":"","address":"","status":"active","add_outlet":""}

View File

@ -1,18 +1,18 @@
Plan:
I have restored the "Manage Outlets" feature.
1. Identified a critical HTML structure error: a missing `</tbody>` tag in the `accounting` journal table (index.php) was causing the browser to render the page incorrectly, potentially hiding the main content area.
2. Found a JavaScript logic error in the `renderPagination` function where `document.currentScript` was being accessed inside a `DOMContentLoaded` listener (where it is null), causing the "Rows per page" dropdown to remain at the bottom instead of moving above the table.
3. Fixed both issues by patching `index.php`.
**Plan:**
1. **Controller Logic:** Added the backend logic to handle `outlets` page requests (listing, adding, editing, deleting outlets) in `index.php`.
2. **Navigation:** Added a "Manage Outlets" link to the sidebar under the **Admin** section.
3. **View Integration:** Re-enabled the `outlets_html.php` view file to display the outlet management interface.
4. **Correction:** Cleaned up some accidental code insertions that occurred during the process.
Changed:
**Changed:**
* `index.php`: Added `case 'outlets':` logic, sidebar link, and view inclusion.
* `index.php`:
* Added the missing `</tbody>` tag to the accounting journal table.
* Updated the `renderPagination` inline script to correctly capture `document.currentScript` immediately and safely move the "Rows per page" dropdown above the table wrapper.
**Notes:**
* You can now access **Manage Outlets** from the sidebar (under the Admin section).
* The system supports multiple outlets as requested.
* Only Administrators can access this page.
Notes:
* The missing `</tbody>` tag likely caused the browser to treat the entire main content area as invalid or hidden, explaining why only the sidebar was visible.
* The pagination dropdown should now appear correctly above the table.
Next: Please verify that the accounting page (`/index.php?page=accounting`) now loads correctly with the sidebar AND the main content table. Also check if the "Rows per page" dropdown is visible above the table.
**Next:**
Please verify if you can now see the "Manage Outlets" link and manage your outlets successfully.