Autosave: 20260327-034455

This commit is contained in:
Flatlogic Bot 2026-03-27 03:44:56 +00:00
parent d38b70650f
commit 110626a686
8 changed files with 1088 additions and 4 deletions

View File

@ -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; }
}

View File

@ -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;

194
expense_categories.php Normal file
View File

@ -0,0 +1,194 @@
<?php
require_once __DIR__ . '/includes/header.php';
if (!canView('expense_settings')) {
redirect('index.php');
}
$error = '';
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!canEdit('expense_settings')) redirect('expense_categories.php');
$action = $_POST['action'] ?? '';
$id = $_POST['id'] ?? 0;
$name = trim($_POST['name'] ?? '');
$description = trim($_POST['description'] ?? '');
if ($name) {
try {
$db = db();
if ($action === 'add') {
$stmt = $db->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']);
}
?>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">إعدادات تصنيفات المصروفات</h1>
<?php if (canAdd('expense_settings')): ?>
<button type="button" class="btn btn-primary shadow-sm" onclick="openModal('add')">
<i class="fas fa-plus"></i> إضافة تصنيف جديد
</button>
<?php endif; ?>
</div>
<?php if ($success): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?= $success ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if ($error): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?= $error ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="card shadow-sm border-0">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">اسم التصنيف</th>
<th>الوصف</th>
<th class="text-center">الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($categories as $cat): ?>
<tr>
<td class="ps-4 fw-bold"><?= htmlspecialchars($cat['name']) ?></td>
<td><?= htmlspecialchars($cat['description']) ?></td>
<td class="text-center">
<?php if (canEdit('expense_settings')): ?>
<button class="btn btn-sm btn-outline-primary" onclick='openModal("edit", <?= json_encode($cat, JSON_HEX_APOS | JSON_HEX_QUOT) ?>)'>
<i class="fas fa-edit"></i>
</button>
<?php endif; ?>
<?php if (canDelete('expense_settings')): ?>
<a href="javascript:void(0)" onclick="confirmDelete(<?= $cat['id'] ?>)" class="btn btn-sm btn-outline-danger">
<i class="fas fa-trash"></i>
</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="categoryModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="modalTitle">تصنيف جديد</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST">
<div class="modal-body">
<input type="hidden" name="action" id="modalAction" value="add">
<input type="hidden" name="id" id="modalId" value="0">
<div class="mb-3">
<label class="form-label fw-bold">اسم التصنيف</label>
<input type="text" name="name" id="modalName" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label fw-bold">الوصف</label>
<textarea name="description" id="modalDescription" class="form-control" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
<button type="submit" class="btn btn-primary">حفظ</button>
</div>
</form>
</div>
</div>
</div>
<script>
let categoryModal;
function openModal(action, data = null) {
if (!categoryModal) {
categoryModal = new bootstrap.Modal(document.getElementById('categoryModal'));
}
document.getElementById('modalAction').value = action;
const title = document.getElementById('modalTitle');
if (action === 'add') {
title.textContent = 'تصنيف جديد';
document.getElementById('modalId').value = 0;
document.getElementById('modalName').value = '';
document.getElementById('modalDescription').value = '';
} else {
title.textContent = 'تعديل التصنيف';
document.getElementById('modalId').value = data.id;
document.getElementById('modalName').value = data.name;
document.getElementById('modalDescription').value = data.description;
}
categoryModal.show();
}
function confirmDelete(id) {
if (confirm('هل أنت متأكد من الحذف؟')) {
window.location.href = 'expense_categories.php?action=delete&id=' + id;
}
}
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

199
expense_reports.php Normal file
View File

@ -0,0 +1,199 @@
<?php
require_once __DIR__ . '/includes/header.php';
if (!canView('expenses')) {
redirect('index.php');
}
// Params
$date_from = $_GET['date_from'] ?? date('Y-m-01');
$date_to = $_GET['date_to'] ?? date('Y-m-t');
$category_id = $_GET['category_id'] ?? '';
$vendor = $_GET['vendor'] ?? '';
$payment_method = $_GET['payment_method'] ?? '';
// Build Query
$sql = "SELECT e.*, c.name as category_name, u.username as created_by
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_id) {
$sql .= " AND e.category_id = ?";
$params[] = $category_id;
}
if ($vendor) {
$sql .= " AND e.vendor LIKE ?";
$params[] = "%$vendor%";
}
if ($payment_method) {
$sql .= " AND e.payment_method = ?";
$params[] = $payment_method;
}
$sql .= " ORDER BY e.date ASC";
$stmt = db()->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);
?>
<style>
@media print {
.no-print { display: none !important; }
.sidebar, .top-navbar { display: none !important; }
.main-content { margin: 0 !important; padding: 0 !important; }
.card { border: none !important; shadow: none !important; }
body { background: white !important; }
}
</style>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom no-print">
<h1 class="h2">تقارير المصروفات</h1>
<button onclick="window.print()" class="btn btn-outline-secondary">
<i class="fas fa-print"></i> طباعة التقرير
</button>
</div>
<!-- Filters -->
<div class="card shadow-sm border-0 mb-4 no-print">
<div class="card-body bg-light">
<form method="GET" class="row g-3">
<div class="col-md-3">
<label class="form-label">من تاريخ</label>
<input type="date" name="date_from" class="form-control" value="<?= $date_from ?>">
</div>
<div class="col-md-3">
<label class="form-label">إلى تاريخ</label>
<input type="date" name="date_to" class="form-control" value="<?= $date_to ?>">
</div>
<div class="col-md-3">
<label class="form-label">التصنيف</label>
<select name="category_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>" <?= $category_id == $cat['id'] ? 'selected' : '' ?>><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label">طريقة الدفع</label>
<select name="payment_method" class="form-select">
<option value="">الكل</option>
<option value="Cash" <?= $payment_method == 'Cash' ? 'selected' : '' ?>>نقد</option>
<option value="Bank Transfer" <?= $payment_method == 'Bank Transfer' ? 'selected' : '' ?>>تحويل بنكي</option>
<option value="Credit Card" <?= $payment_method == 'Credit Card' ? 'selected' : '' ?>>بطاقة ائتمان</option>
<option value="Check" <?= $payment_method == 'Check' ? 'selected' : '' ?>>شيك</option>
</select>
</div>
<div class="col-md-12 text-end">
<button type="submit" class="btn btn-primary px-4"><i class="fas fa-filter me-1"></i> عرض التقرير</button>
</div>
</form>
</div>
</div>
<!-- Report Header (Print Only) -->
<div class="d-none d-print-block text-center mb-4">
<h3>تقرير المصروفات التفصيلي</h3>
<p class="text-muted">الفترة من <?= $date_from ?> إلى <?= $date_to ?></p>
</div>
<!-- Summary Cards -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card shadow-sm border-0 h-100 bg-primary text-white">
<div class="card-body text-center">
<h6 class="text-white-50 mb-2">إجمالي المصروفات</h6>
<h2 class="fw-bold mb-0"><?= number_format($total_amount, 2) ?> ر.س</h2>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="text-muted mb-3">ملخص حسب التصنيف</h6>
<div class="row g-2">
<?php foreach ($category_breakdown as $name => $amount): ?>
<div class="col-6 col-md-4">
<div class="p-2 border rounded bg-light">
<small class="d-block text-muted"><?= htmlspecialchars($name) ?></small>
<span class="fw-bold"><?= number_format($amount, 2) ?></span>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
<!-- Detailed Table -->
<div class="card shadow-sm border-0">
<div class="card-header bg-white border-bottom py-3">
<h5 class="mb-0">سجل العمليات</h5>
</div>
<div class="table-responsive">
<table class="table table-bordered align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-3">التاريخ</th>
<th>التصنيف</th>
<th>الوصف</th>
<th>المورد</th>
<th>المرجع</th>
<th>طريقة الدفع</th>
<th>بواسطة</th>
<th class="text-end pe-3">المبلغ</th>
</tr>
</thead>
<tbody>
<?php if (empty($expenses)): ?>
<tr>
<td colspan="8" class="text-center py-4 text-muted">لا توجد بيانات للفترة المحددة</td>
</tr>
<?php else: ?>
<?php foreach ($expenses as $exp): ?>
<tr>
<td class="ps-3"><?= $exp['date'] ?></td>
<td><?= htmlspecialchars($exp['category_name']) ?></td>
<td><?= htmlspecialchars($exp['description']) ?></td>
<td><?= htmlspecialchars($exp['vendor'] ?: '-') ?></td>
<td><?= htmlspecialchars($exp['reference'] ?: '-') ?></td>
<td><?= htmlspecialchars($exp['payment_method']) ?></td>
<td><small><?= htmlspecialchars($exp['created_by'] ?: '-') ?></small></td>
<td class="text-end pe-3 fw-bold"><?= number_format($exp['amount'], 2) ?></td>
</tr>
<?php endforeach; ?>
<tr class="bg-light fw-bold">
<td colspan="7" class="text-end ps-3">الإجمالي النهائي:</td>
<td class="text-end pe-3 text-danger"><?= number_format($total_amount, 2) ?></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<div class="mt-4 text-center text-muted small d-none d-print-block">
تم استخراج التقرير في <?= date('Y-m-d H:i:s') ?> بواسطة <?= $_SESSION['name'] ?? 'System' ?>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

367
expenses.php Normal file
View File

@ -0,0 +1,367 @@
<?php
require_once __DIR__ . '/includes/header.php';
if (!canView('expenses')) {
redirect('index.php');
}
$error = '';
$success = '';
// Handle Actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
$id = $_POST['id'] ?? 0;
if ($action === 'add' || $action === 'edit') {
if (($action === 'add' && !canAdd('expenses')) || ($action === 'edit' && !canEdit('expenses'))) {
$error = 'ليس لديك صلاحية للقيام بهذا الإجراء';
} else {
$date = $_POST['date'] ?? date('Y-m-d');
$category_id = $_POST['category_id'] ?? 0;
$amount = $_POST['amount'] ?? 0;
$description = $_POST['description'] ?? '';
$reference = $_POST['reference'] ?? '';
$vendor = $_POST['vendor'] ?? '';
$payment_method = $_POST['payment_method'] ?? 'Cash';
// File Upload
$receipt_path = null;
if (isset($_FILES['receipt']) && $_FILES['receipt']['error'] === UPLOAD_ERR_OK) {
$upload_dir = 'uploads/receipts/';
if (!is_dir($upload_dir)) mkdir($upload_dir, 0775, true);
$ext = pathinfo($_FILES['receipt']['name'], PATHINFO_EXTENSION);
$filename = time() . '_' . uniqid() . '.' . $ext;
$target = $upload_dir . $filename;
if (move_uploaded_file($_FILES['receipt']['tmp_name'], $target)) {
$receipt_path = $target;
}
}
try {
$db = db();
if ($action === 'add') {
$stmt = $db->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']);
}
?>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">سجل المصروفات</h1>
<?php if (canAdd('expenses')): ?>
<button type="button" class="btn btn-primary shadow-sm" onclick="openModal('add')">
<i class="fas fa-plus"></i> تسجيل مصروف جديد
</button>
<?php endif; ?>
</div>
<?php if ($success): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?= $success ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if ($error): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?= $error ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<!-- Filters -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-body bg-light">
<form method="GET" class="row g-3">
<div class="col-md-3">
<label class="form-label">من تاريخ</label>
<input type="date" name="date_from" class="form-control" value="<?= $date_from ?>">
</div>
<div class="col-md-3">
<label class="form-label">إلى تاريخ</label>
<input type="date" name="date_to" class="form-control" value="<?= $date_to ?>">
</div>
<div class="col-md-3">
<label class="form-label">التصنيف</label>
<select name="category_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>" <?= $category_filter == $cat['id'] ? 'selected' : '' ?>><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label">بحث</label>
<div class="input-group">
<input type="text" name="search" class="form-control" placeholder="وصف، مورد، مرجع..." value="<?= htmlspecialchars($search) ?>">
<button class="btn btn-primary" type="submit"><i class="fas fa-search"></i></button>
</div>
</div>
</form>
</div>
</div>
<!-- Table -->
<div class="card shadow-sm border-0">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">التاريخ</th>
<th>التصنيف</th>
<th>الوصف</th>
<th>المورد</th>
<th>المبلغ</th>
<th>طريقة الدفع</th>
<th>الإيصال</th>
<th class="text-center">الإجراءات</th>
</tr>
</thead>
<tbody>
<?php if (empty($expenses)): ?>
<tr>
<td colspan="8" class="text-center py-5 text-muted">لا توجد سجلات مطابقة</td>
</tr>
<?php else: ?>
<?php foreach ($expenses as $exp): ?>
<tr>
<td class="ps-4"><?= $exp['date'] ?></td>
<td><span class="badge bg-secondary bg-opacity-10 text-secondary"><?= htmlspecialchars($exp['category_name']) ?></span></td>
<td>
<?= htmlspecialchars($exp['description']) ?>
<?php if ($exp['reference']): ?>
<br><small class="text-muted">Ref: <?= htmlspecialchars($exp['reference']) ?></small>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($exp['vendor'] ?: '-') ?></td>
<td class="fw-bold text-danger"><?= number_format($exp['amount'], 2) ?></td>
<td><?= htmlspecialchars($exp['payment_method']) ?></td>
<td>
<?php if ($exp['receipt_file']): ?>
<a href="<?= htmlspecialchars($exp['receipt_file']) ?>" target="_blank" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-paperclip"></i>
</a>
<?php else: ?>
-
<?php endif; ?>
</td>
<td class="text-center">
<?php if (canEdit('expenses')): ?>
<button class="btn btn-sm btn-outline-primary" onclick='openModal("edit", <?= json_encode($exp, JSON_HEX_APOS | JSON_HEX_QUOT) ?>)'>
<i class="fas fa-edit"></i>
</button>
<?php endif; ?>
<?php if (canDelete('expenses')): ?>
<a href="javascript:void(0)" onclick="confirmDelete(<?= $exp['id'] ?>)" class="btn btn-sm btn-outline-danger">
<i class="fas fa-trash"></i>
</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<tr class="bg-light fw-bold">
<td colspan="4" class="text-end">الإجمالي:</td>
<td class="text-danger"><?= number_format(array_sum(array_column($expenses, 'amount')), 2) ?></td>
<td colspan="3"></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="expenseModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="modalTitle">تسجيل مصروف جديد</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" enctype="multipart/form-data">
<div class="modal-body">
<input type="hidden" name="action" id="modalAction" value="add">
<input type="hidden" name="id" id="modalId" value="0">
<div class="mb-3">
<label class="form-label fw-bold">التاريخ</label>
<input type="date" name="date" id="modalDate" class="form-control" required value="<?= date('Y-m-d') ?>">
</div>
<div class="mb-3">
<label class="form-label fw-bold">التصنيف</label>
<select name="category_id" id="modalCategory" class="form-select" required>
<option value="">اختر التصنيف...</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>"><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">المبلغ</label>
<input type="number" step="0.01" name="amount" id="modalAmount" class="form-control" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">طريقة الدفع</label>
<select name="payment_method" id="modalPaymentMethod" class="form-select">
<option value="Cash">نقد (Cash)</option>
<option value="Bank Transfer">تحويل بنكي</option>
<option value="Credit Card">بطاقة ائتمان</option>
<option value="Check">شيك</option>
<option value="Other">أخرى</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">المورد / المستفيد</label>
<input type="text" name="vendor" id="modalVendor" class="form-control" placeholder="اسم المحل، الشركة، الشخص...">
</div>
<div class="mb-3">
<label class="form-label fw-bold">الوصف</label>
<textarea name="description" id="modalDescription" class="form-control" rows="2"></textarea>
</div>
<div class="mb-3">
<label class="form-label fw-bold">رقم مرجعي (اختياري)</label>
<input type="text" name="reference" id="modalReference" class="form-control" placeholder="رقم فاتورة، رقم إيصال...">
</div>
<div class="mb-3">
<label class="form-label fw-bold">صورة الإيصال (اختياري)</label>
<input type="file" name="receipt" class="form-control" accept="image/*,.pdf">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
<button type="submit" class="btn btn-primary">حفظ</button>
</div>
</form>
</div>
</div>
</div>
<script>
let expenseModal;
function openModal(action, data = null) {
if (!expenseModal) {
expenseModal = new bootstrap.Modal(document.getElementById('expenseModal'));
}
document.getElementById('modalAction').value = action;
const title = document.getElementById('modalTitle');
if (action === 'add') {
title.textContent = 'تسجيل مصروف جديد';
document.getElementById('modalId').value = 0;
document.getElementById('modalAmount').value = '';
document.getElementById('modalDescription').value = '';
document.getElementById('modalVendor').value = '';
document.getElementById('modalReference').value = '';
document.getElementById('modalDate').value = new Date().toISOString().split('T')[0];
document.getElementById('modalCategory').value = '';
document.getElementById('modalPaymentMethod').value = 'Cash';
} else {
title.textContent = 'تعديل المصروف';
document.getElementById('modalId').value = data.id;
document.getElementById('modalDate').value = data.date;
document.getElementById('modalCategory').value = data.category_id;
document.getElementById('modalAmount').value = data.amount;
document.getElementById('modalDescription').value = data.description;
document.getElementById('modalVendor').value = data.vendor;
document.getElementById('modalReference').value = data.reference;
document.getElementById('modalPaymentMethod').value = data.payment_method;
}
expenseModal.show();
}
function confirmDelete(id) {
if (confirm('هل أنت متأكد من حذف هذا المصروف؟')) {
window.location.href = 'expenses.php?action=delete&id=' + id;
}
}
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

228
expenses_dashboard.php Normal file
View File

@ -0,0 +1,228 @@
<?php
require_once __DIR__ . '/includes/header.php';
if (!canView('expenses')) {
redirect('index.php');
}
// Helper to get totals
function getExpenseStats($month, $year) {
$db = db();
$start_date = "$year-$month-01";
$end_date = date("Y-m-t", strtotime($start_date));
// Total for month
$stmt = $db->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);
?>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">لوحة تحكم المصروفات</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="expenses.php" class="btn btn-sm btn-primary">
<i class="fas fa-list"></i> عرض السجل
</a>
</div>
</div>
<!-- Stats Cards -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="text-muted mb-2">إجمالي مصروفات هذا الشهر</h6>
<h3 class="fw-bold text-danger mb-0"><?= number_format($current_stats['total'], 2) ?> ر.س</h3>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="text-muted mb-2">أعلى تصنيف للصرف</h6>
<?php if (!empty($current_stats['by_category'])): ?>
<h3 class="fw-bold text-primary mb-0"><?= htmlspecialchars($current_stats['by_category'][0]['name']) ?></h3>
<small class="text-muted"><?= number_format($current_stats['by_category'][0]['total'], 2) ?> ر.س</small>
<?php else: ?>
<h3 class="fw-bold text-muted mb-0">-</h3>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="text-muted mb-2">متوسط الصرف (آخر 6 أشهر)</h6>
<?php
$avg = array_sum(array_column($trend_data, 'total')) / count($trend_data);
?>
<h3 class="fw-bold text-info mb-0"><?= number_format($avg, 2) ?> ر.س</h3>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<!-- Trend Chart -->
<div class="col-md-8 mb-4 mb-md-0">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-transparent border-0 d-flex justify-content-between align-items-center">
<h5 class="mb-0">اتجاه المصروفات (آخر 6 أشهر)</h5>
</div>
<div class="card-body">
<canvas id="trendChart" height="120"></canvas>
</div>
</div>
</div>
<!-- Category Pie Chart -->
<div class="col-md-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-transparent border-0">
<h5 class="mb-0">توزيع المصروفات (<?= date('m/Y') ?>)</h5>
</div>
<div class="card-body position-relative">
<canvas id="categoryChart"></canvas>
</div>
</div>
</div>
</div>
<!-- Recent Expenses -->
<div class="card shadow-sm border-0">
<div class="card-header bg-transparent border-0">
<h5 class="mb-0">آخر المصروفات المسجلة</h5>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">التاريخ</th>
<th>التصنيف</th>
<th>الوصف/المورد</th>
<th>المبلغ</th>
<th>طريقة الدفع</th>
</tr>
</thead>
<tbody>
<?php if (empty($recent_expenses)): ?>
<tr>
<td colspan="5" class="text-center py-4 text-muted">لا توجد مصروفات مسجلة</td>
</tr>
<?php else: ?>
<?php foreach ($recent_expenses as $exp): ?>
<tr>
<td class="ps-4"><?= $exp['date'] ?></td>
<td><span class="badge bg-secondary bg-opacity-10 text-secondary"><?= htmlspecialchars($exp['category_name']) ?></span></td>
<td>
<div class="fw-bold"><?= htmlspecialchars($exp['description'] ?: '-') ?></div>
<div class="small text-muted"><?= htmlspecialchars($exp['vendor'] ?: '') ?></div>
</td>
<td class="fw-bold text-danger"><?= number_format($exp['amount'], 2) ?></td>
<td><?= htmlspecialchars($exp['payment_method']) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Trend Chart
const trendCtx = document.getElementById('trendChart').getContext('2d');
new Chart(trendCtx, {
type: 'bar',
data: {
labels: <?= json_encode(array_column($trend_data, 'display_label')) ?>,
datasets: [{
label: 'المصروفات',
data: <?= json_encode(array_column($trend_data, 'total')) ?>,
backgroundColor: 'rgba(232, 62, 140, 0.6)', // Pinkish
borderColor: 'rgba(232, 62, 140, 1)',
borderWidth: 1,
borderRadius: 4
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false }
},
scales: {
y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.05)' } },
x: { grid: { display: false } }
}
}
});
// Category Chart
const catCtx = document.getElementById('categoryChart').getContext('2d');
const catData = <?= json_encode($current_stats['by_category']) ?>;
new Chart(catCtx, {
type: 'doughnut',
data: {
labels: catData.map(d => d.name),
datasets: [{
data: catData.map(d => d.total),
backgroundColor: [
'#4e73df', '#1cc88a', '#36b9cc', '#f6c23e', '#e74a3b',
'#858796', '#5a5c69', '#fd7e14', '#20c997', '#6f42c1'
],
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '70%',
plugins: {
legend: { position: 'bottom', labels: { boxWidth: 12 } }
}
}
});
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

View File

@ -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);
</li>
<?php endif; ?>
<!-- Expenses Group -->
<?php if (canView('expenses') || canView('expense_settings')): ?>
<li class="nav-item">
<button class="sidebar-group-btn <?= $is_expenses_open ? '' : 'collapsed' ?>" type="button" data-bs-toggle="collapse" data-bs-target="#menu-expenses" aria-expanded="<?= $is_expenses_open ? 'true' : 'false' ?>">
<span class="group-content group-expenses">
<i class="fas fa-money-bill-wave"></i> المصروفات
</span>
<i class="fas fa-chevron-down arrow-icon"></i>
</button>
<div class="collapse <?= $is_expenses_open ? 'show' : '' ?>" id="menu-expenses">
<ul class="nav flex-column">
<?php if (canView('expenses')): ?>
<li class="nav-item">
<a class="nav-link <?= $cp == 'expenses.php' ? 'active' : '' ?>" href="expenses.php">
سجل المصروفات
</a>
</li>
<?php endif; ?>
<?php if (canView('expense_settings')): ?>
<li class="nav-item">
<a class="nav-link <?= $cp == 'expense_categories.php' ? 'active' : '' ?>" href="expense_categories.php">
تصنيفات المصروفات
</a>
</li>
<?php endif; ?>
<?php if (canView('expenses')): ?>
<li class="nav-item">
<a class="nav-link <?= $cp == 'expense_reports.php' ? 'active' : '' ?>" href="expense_reports.php">
التقارير
</a>
</li>
<?php endif; ?>
</ul>
</div>
</li>
<?php endif; ?>
<!-- Reports Group -->
<?php if (canView('reports')): ?>
<li class="nav-item">

View File

@ -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) {
}
</style>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
<?php require_once __DIR__ . '/includes/footer.php';