diff --git a/assets/css/custom.css b/assets/css/custom.css index 166579e..a7832f4 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -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%; -} \ No newline at end of file +} diff --git a/db/migrations/20260318_add_outlet_id_to_purchases.sql b/db/migrations/20260318_add_outlet_id_to_purchases.sql new file mode 100644 index 0000000..1337b35 --- /dev/null +++ b/db/migrations/20260318_add_outlet_id_to_purchases.sql @@ -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; diff --git a/db/migrations/20260318_create_outlets_table.sql b/db/migrations/20260318_create_outlets_table.sql new file mode 100644 index 0000000..3d556b6 --- /dev/null +++ b/db/migrations/20260318_create_outlets_table.sql @@ -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); diff --git a/db/migrations/20260318_multi_outlet_schema.sql b/db/migrations/20260318_multi_outlet_schema.sql new file mode 100644 index 0000000..63d4f6a --- /dev/null +++ b/db/migrations/20260318_multi_outlet_schema.sql @@ -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; diff --git a/db/migrations/20260318_user_outlets_table.sql b/db/migrations/20260318_user_outlets_table.sql new file mode 100644 index 0000000..50efe54 --- /dev/null +++ b/db/migrations/20260318_user_outlets_table.sql @@ -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 +); diff --git a/fix_index_py.py b/fix_index_py.py new file mode 100644 index 0000000..b21ece6 --- /dev/null +++ b/fix_index_py.py @@ -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.") diff --git a/includes/stock_helper.php b/includes/stock_helper.php new file mode 100644 index 0000000..0b4cca5 --- /dev/null +++ b/includes/stock_helper.php @@ -0,0 +1,32 @@ +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]); + } +} \ No newline at end of file diff --git a/index.php b/index.php index 4c70059..4a26cc3 100644 --- a/index.php +++ b/index.php @@ -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';
Maintain your team accounts and security
-| User Info | -Access Level | -Contact | -Status | -Actions | -
|---|---|---|---|---|
|
-
-
-
-
- = strtoupper(substr((string)$u['username'], 0, 1)) ?>
-
-
-
-
- = htmlspecialchars((string)$u['username']) ?>
- ID: #= str_pad((string)$u['id'], 4, '0', STR_PAD_LEFT) ?>
- |
- - - = htmlspecialchars((string)($u['group_name'] ?? 'No Role Assigned')) ?> - - | -
- = htmlspecialchars((string)($u['email'] ?? '')) ?>
- = htmlspecialchars((string)($u['phone'] ?? '-')) ?>
- |
- - - Active - - Suspended - - | -
-
-
-
-
-
-
-
-
-
-
-
- |
-
Maintain your team accounts and security
+| User Info | +Access Level | +Contact | +Status | +Actions | +
|---|---|---|---|---|
|
+
+
+
+
+ = strtoupper(substr((string)$u['username'], 0, 1)) ?>
+
+
+
+
+ = htmlspecialchars((string)$u['username']) ?>
+ ID: #= str_pad((string)$u['id'], 4, '0', STR_PAD_LEFT) ?>
+ |
+ + + = htmlspecialchars((string)($u['group_name'] ?? 'No Role Assigned')) ?> + + | +
+ = htmlspecialchars((string)($u['email'] ?? '')) ?>
+ = htmlspecialchars((string)($u['phone'] ?? '-')) ?>
+ |
+ + + Active + + Suspended + + | +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+