diff --git a/assets/css/custom.css b/assets/css/custom.css
index 8c68d28..b9b0cca 100644
--- a/assets/css/custom.css
+++ b/assets/css/custom.css
@@ -507,6 +507,8 @@ body {
.group-hr { color: #66bb6a !important; } /* Green */
.group-admin { color: #ef5350 !important; } /* Red */
.group-reports { color: #ab47bc !important; }/* Purple */
+.group-stock { color: #fd7e14 !important; } /* Orange */
+.group-expenses { color: #e83e8c !important; } /* Pink */
/* Submenu indentation */
.sidebar .collapse .nav-link {
@@ -518,5 +520,4 @@ body {
.sidebar .collapse .nav-link:hover,
.sidebar .collapse .nav-link.active {
color: #fff;
-}
-.group-stock { color: #fd7e14 !important; }
+}
\ No newline at end of file
diff --git a/db/migrations/022_add_expenses_module.sql b/db/migrations/022_add_expenses_module.sql
new file mode 100644
index 0000000..c86ff28
--- /dev/null
+++ b/db/migrations/022_add_expenses_module.sql
@@ -0,0 +1,53 @@
+-- Expenses module tables
+
+CREATE TABLE IF NOT EXISTS expense_categories (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(100) NOT NULL,
+ description TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS expenses (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ date DATE NOT NULL,
+ category_id INT NOT NULL,
+ amount DECIMAL(15,2) NOT NULL,
+ reference VARCHAR(100),
+ description TEXT,
+ vendor VARCHAR(100),
+ payment_method ENUM('Cash', 'Bank Transfer', 'Credit Card', 'Check', 'Other') DEFAULT 'Cash',
+ receipt_file VARCHAR(255),
+ user_id INT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (category_id) REFERENCES expense_categories(id) ON DELETE RESTRICT
+);
+
+-- Seed default categories
+INSERT INTO expense_categories (name, description) VALUES
+('Office Supplies', 'Pens, paper, toner, etc.'),
+('Travel', 'Flights, hotels, transport'),
+('Meals & Entertainment', 'Client lunches, team events'),
+('Utilities', 'Electricity, water, internet'),
+('Rent', 'Office rent'),
+('Salaries', 'Employee salaries'),
+('Maintenance', 'Repairs and maintenance'),
+('Advertising', 'Marketing and ads'),
+('Software', 'Subscriptions and licenses'),
+('Other', 'Miscellaneous expenses');
+
+-- Add permissions
+INSERT IGNORE INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete)
+SELECT id, 'expenses',
+ IF(role = 'admin', 1, 0),
+ IF(role = 'admin', 1, 0),
+ IF(role = 'admin', 1, 0),
+ IF(role = 'admin', 1, 0)
+FROM users;
+
+INSERT IGNORE INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete)
+SELECT id, 'expense_settings',
+ IF(role = 'admin', 1, 0),
+ IF(role = 'admin', 1, 0),
+ IF(role = 'admin', 1, 0),
+ IF(role = 'admin', 1, 0)
+FROM users;
diff --git a/expense_categories.php b/expense_categories.php
new file mode 100644
index 0000000..d2e82f2
--- /dev/null
+++ b/expense_categories.php
@@ -0,0 +1,194 @@
+prepare("INSERT INTO expense_categories (name, description) VALUES (?, ?)");
+ $stmt->execute([$name, $description]);
+ $_SESSION['success'] = 'تم إضافة التصنيف بنجاح';
+ } elseif ($action === 'edit' && $id) {
+ $stmt = $db->prepare("UPDATE expense_categories SET name = ?, description = ? WHERE id = ?");
+ $stmt->execute([$name, $description, $id]);
+ $_SESSION['success'] = 'تم تحديث التصنيف بنجاح';
+ }
+ redirect('expense_categories.php');
+ } catch (PDOException $e) {
+ $error = 'حدث خطأ: ' . $e->getMessage();
+ }
+ } else {
+ $error = 'اسم التصنيف مطلوب';
+ }
+}
+
+if (isset($_GET['action']) && $_GET['action'] === 'delete' && isset($_GET['id'])) {
+ if (!canDelete('expense_settings')) redirect('expense_categories.php');
+ $id = $_GET['id'];
+ try {
+ $db = db();
+ $stmt = $db->prepare("DELETE FROM expense_categories WHERE id = ?");
+ $stmt->execute([$id]);
+ $_SESSION['success'] = 'تم حذف التصنيف بنجاح';
+ } catch (PDOException $e) {
+ if ($e->getCode() == 23000) {
+ $_SESSION['error'] = 'لا يمكن حذف هذا التصنيف لأنه مرتبط بمصروفات مسجلة';
+ } else {
+ $_SESSION['error'] = 'حدث خطأ: ' . $e->getMessage();
+ }
+ }
+ redirect('expense_categories.php');
+}
+
+$categories = db()->query("SELECT * FROM expense_categories ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
+
+if (isset($_SESSION['success'])) {
+ $success = $_SESSION['success'];
+ unset($_SESSION['success']);
+}
+if (isset($_SESSION['error'])) {
+ $error = $_SESSION['error'];
+ unset($_SESSION['error']);
+}
+?>
+
+
+
إعدادات تصنيفات المصروفات
+
+
+
+
+
+
+
+ = $success ?>
+
+
+
+
+
+
+ = $error ?>
+
+
+
+
+
+
+
+
+
+
+ | اسم التصنيف |
+ الوصف |
+ الإجراءات |
+
+
+
+
+
+ | = htmlspecialchars($cat['name']) ?> |
+ = htmlspecialchars($cat['description']) ?> |
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/expense_reports.php b/expense_reports.php
new file mode 100644
index 0000000..c9c7154
--- /dev/null
+++ b/expense_reports.php
@@ -0,0 +1,199 @@
+prepare($sql);
+$stmt->execute($params);
+$expenses = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+// Calculate Totals
+$total_amount = 0;
+$category_breakdown = [];
+foreach ($expenses as $exp) {
+ $total_amount += $exp['amount'];
+ if (!isset($category_breakdown[$exp['category_name']])) {
+ $category_breakdown[$exp['category_name']] = 0;
+ }
+ $category_breakdown[$exp['category_name']] += $exp['amount'];
+}
+
+// Fetch Categories
+$categories = db()->query("SELECT * FROM expense_categories ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
+
+?>
+
+
+
+
+
تقارير المصروفات
+
+
+
+
+
+
+
+
+
+
+
+
+
تقرير المصروفات التفصيلي
+
الفترة من = $date_from ?> إلى = $date_to ?>
+
+
+
+
+
+
+
+
إجمالي المصروفات
+ = number_format($total_amount, 2) ?> ر.س
+
+
+
+
+
+
+
ملخص حسب التصنيف
+
+ $amount): ?>
+
+
+ = htmlspecialchars($name) ?>
+ = number_format($amount, 2) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | التاريخ |
+ التصنيف |
+ الوصف |
+ المورد |
+ المرجع |
+ طريقة الدفع |
+ بواسطة |
+ المبلغ |
+
+
+
+
+
+ | لا توجد بيانات للفترة المحددة |
+
+
+
+
+ | = $exp['date'] ?> |
+ = htmlspecialchars($exp['category_name']) ?> |
+ = htmlspecialchars($exp['description']) ?> |
+ = htmlspecialchars($exp['vendor'] ?: '-') ?> |
+ = htmlspecialchars($exp['reference'] ?: '-') ?> |
+ = htmlspecialchars($exp['payment_method']) ?> |
+ = htmlspecialchars($exp['created_by'] ?: '-') ?> |
+ = number_format($exp['amount'], 2) ?> |
+
+
+
+ | الإجمالي النهائي: |
+ = number_format($total_amount, 2) ?> |
+
+
+
+
+
+
+
+
+ تم استخراج التقرير في = date('Y-m-d H:i:s') ?> بواسطة = $_SESSION['name'] ?? 'System' ?>
+
+
+
\ No newline at end of file
diff --git a/expenses.php b/expenses.php
new file mode 100644
index 0000000..9d753d8
--- /dev/null
+++ b/expenses.php
@@ -0,0 +1,367 @@
+prepare("INSERT INTO expenses (date, category_id, amount, description, reference, vendor, payment_method, receipt_file, user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
+ $stmt->execute([$date, $category_id, $amount, $description, $reference, $vendor, $payment_method, $receipt_path, $_SESSION['user_id']]);
+ $_SESSION['success'] = 'تم إضافة المصروف بنجاح';
+ } else {
+ // Get old file if not replaced
+ if (!$receipt_path) {
+ $stmt = $db->prepare("SELECT receipt_file FROM expenses WHERE id = ?");
+ $stmt->execute([$id]);
+ $receipt_path = $stmt->fetchColumn();
+ }
+
+ $stmt = $db->prepare("UPDATE expenses SET date=?, category_id=?, amount=?, description=?, reference=?, vendor=?, payment_method=?, receipt_file=? WHERE id=?");
+ $stmt->execute([$date, $category_id, $amount, $description, $reference, $vendor, $payment_method, $receipt_path, $id]);
+ $_SESSION['success'] = 'تم تحديث المصروف بنجاح';
+ }
+ redirect('expenses.php');
+ } catch (PDOException $e) {
+ $error = 'حدث خطأ: ' . $e->getMessage();
+ }
+ }
+ }
+}
+
+if (isset($_GET['action']) && $_GET['action'] === 'delete' && isset($_GET['id'])) {
+ if (!canDelete('expenses')) redirect('expenses.php');
+ $id = $_GET['id'];
+ $db = db();
+ // Get file to delete
+ $stmt = $db->prepare("SELECT receipt_file FROM expenses WHERE id = ?");
+ $stmt->execute([$id]);
+ $file = $stmt->fetchColumn();
+ if ($file && file_exists($file)) unlink($file);
+
+ $stmt = $db->prepare("DELETE FROM expenses WHERE id = ?");
+ $stmt->execute([$id]);
+ $_SESSION['success'] = 'تم حذف المصروف بنجاح';
+ redirect('expenses.php');
+}
+
+// Fetch Data for List
+$date_from = $_GET['date_from'] ?? date('Y-m-01');
+$date_to = $_GET['date_to'] ?? date('Y-m-t');
+$category_filter = $_GET['category_id'] ?? '';
+$search = $_GET['search'] ?? '';
+
+$sql = "SELECT e.*, c.name as category_name, u.username as created_by_name
+ FROM expenses e
+ LEFT JOIN expense_categories c ON e.category_id = c.id
+ LEFT JOIN users u ON e.user_id = u.id
+ WHERE e.date BETWEEN ? AND ?";
+$params = [$date_from, $date_to];
+
+if ($category_filter) {
+ $sql .= " AND e.category_id = ?";
+ $params[] = $category_filter;
+}
+if ($search) {
+ $sql .= " AND (e.description LIKE ? OR e.vendor LIKE ? OR e.reference LIKE ?)";
+ $params[] = "%$search%";
+ $params[] = "%$search%";
+ $params[] = "%$search%";
+}
+
+$sql .= " ORDER BY e.date DESC, e.id DESC";
+
+$stmt = db()->prepare($sql);
+$stmt->execute($params);
+$expenses = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+// Fetch Categories for Dropdown
+$categories = db()->query("SELECT * FROM expense_categories ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
+
+if (isset($_SESSION['success'])) {
+ $success = $_SESSION['success'];
+ unset($_SESSION['success']);
+}
+?>
+
+
+
سجل المصروفات
+
+
+
+
+
+
+
+ = $success ?>
+
+
+
+
+
+
+ = $error ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | التاريخ |
+ التصنيف |
+ الوصف |
+ المورد |
+ المبلغ |
+ طريقة الدفع |
+ الإيصال |
+ الإجراءات |
+
+
+
+
+
+ | لا توجد سجلات مطابقة |
+
+
+
+
+ | = $exp['date'] ?> |
+ = htmlspecialchars($exp['category_name']) ?> |
+
+ = htmlspecialchars($exp['description']) ?>
+
+ Ref: = htmlspecialchars($exp['reference']) ?>
+
+ |
+ = htmlspecialchars($exp['vendor'] ?: '-') ?> |
+ = number_format($exp['amount'], 2) ?> |
+ = htmlspecialchars($exp['payment_method']) ?> |
+
+
+
+
+
+
+ -
+
+ |
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ | الإجمالي: |
+ = number_format(array_sum(array_column($expenses, 'amount')), 2) ?> |
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/expenses_dashboard.php b/expenses_dashboard.php
new file mode 100644
index 0000000..6fa2d16
--- /dev/null
+++ b/expenses_dashboard.php
@@ -0,0 +1,228 @@
+prepare("SELECT SUM(amount) FROM expenses WHERE date BETWEEN ? AND ?");
+ $stmt->execute([$start_date, $end_date]);
+ $total = $stmt->fetchColumn() ?: 0;
+
+ // By Category
+ $stmt = $db->prepare("SELECT c.name, SUM(e.amount) as total
+ FROM expenses e
+ JOIN expense_categories c ON e.category_id = c.id
+ WHERE e.date BETWEEN ? AND ?
+ GROUP BY c.name
+ ORDER BY total DESC");
+ $stmt->execute([$start_date, $end_date]);
+ $by_category = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ return ['total' => $total, 'by_category' => $by_category];
+}
+
+// Current month stats
+$current_month = date('m');
+$current_year = date('Y');
+$current_stats = getExpenseStats($current_month, $current_year);
+
+// Last 6 months trend
+$trend_data = [];
+for ($i = 5; $i >= 0; $i--) {
+ $d = strtotime("-$i months");
+ $m = date('m', $d);
+ $y = date('Y', $d);
+ $s = getExpenseStats($m, $y);
+ $trend_data[] = [
+ 'label' => date('M Y', $d), // English month names for Chart.js
+ 'display_label' => date('m/Y', $d),
+ 'total' => $s['total']
+ ];
+}
+
+// Recent Expenses
+$stmt = db()->query("SELECT e.*, c.name as category_name
+ FROM expenses e
+ JOIN expense_categories c ON e.category_id = c.id
+ ORDER BY e.date DESC, e.id DESC LIMIT 5");
+$recent_expenses = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+?>
+
+
+
لوحة تحكم المصروفات
+
+
+
+
+
+
+
+
+
إجمالي مصروفات هذا الشهر
+ = number_format($current_stats['total'], 2) ?> ر.س
+
+
+
+
+
+
+
أعلى تصنيف للصرف
+
+ = htmlspecialchars($current_stats['by_category'][0]['name']) ?>
+ = number_format($current_stats['by_category'][0]['total'], 2) ?> ر.س
+
+ -
+
+
+
+
+
+
+
+
متوسط الصرف (آخر 6 أشهر)
+
+ = number_format($avg, 2) ?> ر.س
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | التاريخ |
+ التصنيف |
+ الوصف/المورد |
+ المبلغ |
+ طريقة الدفع |
+
+
+
+
+
+ | لا توجد مصروفات مسجلة |
+
+
+
+
+ | = $exp['date'] ?> |
+ = htmlspecialchars($exp['category_name']) ?> |
+
+ = htmlspecialchars($exp['description'] ?: '-') ?>
+ = htmlspecialchars($exp['vendor'] ?: '') ?>
+ |
+ = number_format($exp['amount'], 2) ?> |
+ = htmlspecialchars($exp['payment_method']) ?> |
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/includes/header.php b/includes/header.php
index 7150c72..d4b0c1b 100644
--- a/includes/header.php
+++ b/includes/header.php
@@ -130,6 +130,9 @@ $is_hr_open = in_array($cp, $hr_pages);
$stock_pages = ['stock_dashboard.php', 'stock_items.php', 'stock_in.php', 'stock_out.php', 'stock_lending.php', 'stock_reports.php', 'stock_settings.php'];
$is_stock_open = in_array($cp, $stock_pages);
+$expenses_pages = ['expenses.php', 'expense_categories.php', 'expense_reports.php'];
+$is_expenses_open = in_array($cp, $expenses_pages);
+
$report_pages = ['overdue_report.php'];
$is_report_open = in_array($cp, $report_pages);
@@ -482,6 +485,43 @@ $is_admin_open = in_array($cp, $admin_pages);
+
+
+
+
+
+
+
+
diff --git a/users.php b/users.php
index a9082f0..5dd54b8 100644
--- a/users.php
+++ b/users.php
@@ -28,7 +28,9 @@ $modules = [
'stock_out' => 'المخزون - صرف (صادر)',
'stock_lending' => 'المخزون - الإعارة',
'stock_reports' => 'المخزون - التقارير',
- 'stock_settings' => 'المخزون - الإعدادات'
+ 'stock_settings' => 'المخزون - الإعدادات',
+ 'expenses' => 'المصروفات',
+ 'expense_settings' => 'المصروفات - الإعدادات'
];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
@@ -518,4 +520,4 @@ function confirmDelete(id) {
}
-
\ No newline at end of file
+