From 110626a68669d466dcb4146a2cb8fb4095d5efdb Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 27 Mar 2026 03:44:56 +0000 Subject: [PATCH] Autosave: 20260327-034455 --- assets/css/custom.css | 5 +- db/migrations/022_add_expenses_module.sql | 53 ++++ expense_categories.php | 194 ++++++++++++ expense_reports.php | 199 ++++++++++++ expenses.php | 367 ++++++++++++++++++++++ expenses_dashboard.php | 228 ++++++++++++++ includes/header.php | 40 +++ users.php | 6 +- 8 files changed, 1088 insertions(+), 4 deletions(-) create mode 100644 db/migrations/022_add_expenses_module.sql create mode 100644 expense_categories.php create mode 100644 expense_reports.php create mode 100644 expenses.php create mode 100644 expenses_dashboard.php 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']); +} +?> + +
+

إعدادات تصنيفات المصروفات

+ + + +
+ + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + +
اسم التصنيفالوصفالإجراءات
+ + + + + + + + +
+
+
+
+ + + + + + + \ 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); + +?> + + + +
+

تقارير المصروفات

+ +
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + +
+

تقرير المصروفات التفصيلي

+

الفترة من إلى

+
+ + +
+
+
+
+
إجمالي المصروفات
+

ر.س

+
+
+
+
+
+
+
ملخص حسب التصنيف
+
+ $amount): ?> +
+
+ + +
+
+ +
+
+
+
+
+ + +
+
+
سجل العمليات
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
التاريخالتصنيفالوصفالموردالمرجعطريقة الدفعبواسطةالمبلغ
لا توجد بيانات للفترة المحددة
الإجمالي النهائي:
+
+
+ +
+ تم استخراج التقرير في بواسطة +
+ + \ 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']); +} +?> + +
+

سجل المصروفات

+ + + +
+ + + + + + + + + + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
التاريخالتصنيفالوصفالموردالمبلغطريقة الدفعالإيصالالإجراءات
لا توجد سجلات مطابقة
+ + +
Ref: + +
+ + + + + + - + + + + + + + + + + +
الإجمالي:
+
+
+
+ + + + + + + \ 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); + +?> + +
+

لوحة تحكم المصروفات

+ +
+ + +
+
+
+
+
إجمالي مصروفات هذا الشهر
+

ر.س

+
+
+
+
+
+
+
أعلى تصنيف للصرف
+ +

+ ر.س + +

-

+ +
+
+
+
+
+
+
متوسط الصرف (آخر 6 أشهر)
+ +

ر.س

+
+
+
+
+ +
+ +
+
+
+
اتجاه المصروفات (آخر 6 أشهر)
+
+
+ +
+
+
+ + +
+
+
+
توزيع المصروفات ()
+
+
+ +
+
+
+
+ + +
+
+
آخر المصروفات المسجلة
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
التاريخالتصنيفالوصف/الموردالمبلغطريقة الدفع
لا توجد مصروفات مسجلة
+
+
+
+
+
+ + + + + \ 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); + + + + +