Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79a5056e8e | ||
|
|
ebc8803ddd | ||
|
|
a5a5101374 | ||
|
|
cf34144fdb | ||
|
|
7828e2ad4e | ||
|
|
1b3577b917 | ||
|
|
4ee179ebfe | ||
|
|
07691b3ce6 | ||
|
|
10d64f8648 | ||
|
|
518c9fdd19 | ||
|
|
e11d717222 | ||
|
|
55701e7000 | ||
|
|
110626a686 | ||
|
|
d38b70650f | ||
|
|
24ab40453b | ||
|
|
91600dc6f7 | ||
|
|
17c4e0e1ef | ||
|
|
d82e9249b7 | ||
|
|
cc73fe5d8d | ||
|
|
b573f5ceaa | ||
|
|
2a4914959c | ||
|
|
813c9596f7 | ||
|
|
6ede6271b7 | ||
|
|
36d1266f5e | ||
|
|
9e429aea35 | ||
|
|
85c1b9dd67 | ||
|
|
ad6d09dcf3 | ||
|
|
efe588ff68 | ||
|
|
e9893232d6 | ||
|
|
627842bf5c | ||
|
|
6ddb4f9f37 | ||
|
|
dcb1aa0c6b | ||
|
|
c5ac8e3c6e | ||
|
|
c6cb25129c | ||
|
|
26c455a51e | ||
|
|
e93f08d5e0 | ||
|
|
fcd101fb65 | ||
|
|
b304da2bea | ||
|
|
10eedb49b2 | ||
|
|
d2bc35de8c | ||
|
|
cfc04e4af0 | ||
|
|
6f244ec88a | ||
|
|
37abbe5d1e | ||
|
|
607b9d8838 | ||
|
|
3aa10f537e | ||
|
|
e1febf7e84 | ||
|
|
fe46d13ff0 | ||
|
|
90db1dc8a5 | ||
|
|
21d9204f3a | ||
|
|
118aae16b0 | ||
|
|
e70adf8720 | ||
|
|
2900795488 | ||
|
|
6d5518a7b7 | ||
|
|
fc07d00bbb | ||
|
|
5d112ecb1b |
372
accounting.php
Normal file
372
accounting.php
Normal file
@ -0,0 +1,372 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
require_once 'includes/header.php';
|
||||
require_once 'includes/accounting_functions.php';
|
||||
require_once 'includes/pagination.php';
|
||||
|
||||
// Check permission
|
||||
$user_id = $_SESSION['user_id'] ?? 0;
|
||||
if (!$user_id) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = db()->prepare("SELECT * FROM user_permissions WHERE user_id = ? AND page = 'accounting' AND can_view = 1");
|
||||
$stmt->execute([$user_id]);
|
||||
if (!$stmt->fetch()) {
|
||||
echo "<div class='container mt-4' dir='rtl'>لا تملك صلاحية الوصول لهذه الصفحة.</div>";
|
||||
require_once 'includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_POST['add_entry'])) {
|
||||
$date = $_POST["date"] ?? "";
|
||||
$description = $_POST["description"] ?? "";
|
||||
$reference = $_POST["reference"] ?? "";
|
||||
$entries = [
|
||||
["account" => $_POST["debit_account"] ?? "", "debit" => (float)($_POST["amount"] ?? 0), "credit" => 0],
|
||||
["account" => $_POST["credit_account"] ?? "", "debit" => 0, "credit" => (float)($_POST["amount"] ?? 0)]
|
||||
];
|
||||
|
||||
if (add_journal_entry($date, $description, $reference, $entries)) {
|
||||
$message = "تم إضافة القيد بنجاح.";
|
||||
} else {
|
||||
$error = "حدث خطأ أثناء إضافة القيد.";
|
||||
}
|
||||
} elseif (isset($_POST['edit_entry'])) {
|
||||
$id_to_edit = (int)$_POST['edit_id'];
|
||||
$date = $_POST["date"] ?? "";
|
||||
$description = $_POST["description"] ?? "";
|
||||
$reference = $_POST["reference"] ?? "";
|
||||
$entries = [
|
||||
["account" => $_POST["debit_account"] ?? "", "debit" => (float)($_POST["amount"] ?? 0), "credit" => 0],
|
||||
["account" => $_POST["credit_account"] ?? "", "debit" => 0, "credit" => (float)($_POST["amount"] ?? 0)]
|
||||
];
|
||||
|
||||
if (edit_journal_entry($id_to_edit, $date, $description, $reference, $entries)) {
|
||||
$message = "تم تعديل القيد بنجاح.";
|
||||
} else {
|
||||
$error = "حدث خطأ أثناء تعديل القيد.";
|
||||
}
|
||||
} elseif (isset($_POST['delete_entry'])) {
|
||||
$id_to_delete = (int)$_POST['delete_id'];
|
||||
if (delete_journal_entry($id_to_delete)) {
|
||||
$message = "تم حذف القيد بنجاح.";
|
||||
} else {
|
||||
$error = "حدث خطأ أثناء حذف القيد.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination and Filtering setup
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1; // Standardized to 'page'
|
||||
if ($page < 1) $page = 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$search = $_GET['search'] ?? '';
|
||||
$date_from = $_GET['date_from'] ?? '';
|
||||
$date_to = $_GET['date_to'] ?? '';
|
||||
|
||||
// Fetch ledger data with filters using optimized functions
|
||||
$totalFiltered = get_ledger_count($search, $date_from, $date_to);
|
||||
$ledger = get_ledger_paginated($search, $date_from, $date_to, $limit, $offset);
|
||||
|
||||
?>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<style>
|
||||
.table td, .table th { padding: 0.3rem 0.5rem; }
|
||||
.action-icon { background: none; border: none; padding: 0; margin: 0; cursor: pointer; display: inline-block; }
|
||||
</style>
|
||||
|
||||
<div class="container mt-4" dir="rtl">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="text-right">المحاسبة (Accounting)</h2>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#journalModal">
|
||||
<i class="fas fa-plus"></i> إضافة قيد جديد
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filter Form -->
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-body py-2">
|
||||
<form method="GET" class="row g-2 align-items-center">
|
||||
<div class="col-md-5">
|
||||
<input type="text" name="search" class="form-control form-control-sm" placeholder="بحث..." value="<?= htmlspecialchars($search) ?>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" name="date_from" class="form-control form-control-sm" value="<?= htmlspecialchars($date_from) ?>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" name="date_to" class="form-control form-control-sm" value="<?= htmlspecialchars($date_to) ?>">
|
||||
</div>
|
||||
<div class="col-md-3 text-end">
|
||||
<button type="submit" class="btn btn-sm btn-secondary"><i class="fas fa-filter"></i> تصفية</button>
|
||||
<a href="accounting.php" class="btn btn-sm btn-outline-secondary"><i class="fas fa-sync"></i></a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Journal Modal (Add) -->
|
||||
<div class="modal fade" id="journalModal" tabindex="-1" aria-labelledby="journalModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form method="POST">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="journalModalLabel">إضافة قيد محاسبي</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="add_entry" value="1">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label>التاريخ</label>
|
||||
<input type="date" name="date" class="form-control" required value="<?= date('Y-m-d') ?>">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label>المرجع</label>
|
||||
<input type="text" name="reference" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label>الوصف</label>
|
||||
<input type="text" name="description" class="form-control" required>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label>حساب المدين</label>
|
||||
<select name="debit_account" class="form-select accountSelect" required style="width: 100%;">
|
||||
<option value="" disabled selected>اختر الحساب...</option>
|
||||
<?php foreach (get_all_accounts() as $acc): ?>
|
||||
<option value="<?= htmlspecialchars($acc['name']) ?>"><?= htmlspecialchars($acc['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label>حساب الدائن</label>
|
||||
<select name="credit_account" class="form-select accountSelect" required style="width: 100%;">
|
||||
<option value="" disabled selected>اختر الحساب...</option>
|
||||
<?php foreach (get_all_accounts() as $acc): ?>
|
||||
<option value="<?= htmlspecialchars($acc['name']) ?>"><?= htmlspecialchars($acc['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label>المبلغ</label>
|
||||
<input type="number" step="0.01" name="amount" class="form-control" required min="0.01">
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- Edit Journal Modal -->
|
||||
<div class="modal fade" id="editJournalModal" tabindex="-1" aria-labelledby="editJournalModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form method="POST">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editJournalModalLabel">تعديل قيد محاسبي</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="edit_entry" value="1">
|
||||
<input type="hidden" name="edit_id" id="edit_id" value="">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label>التاريخ</label>
|
||||
<input type="date" name="date" id="edit_date" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label>المرجع</label>
|
||||
<input type="text" name="reference" id="edit_reference" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label>الوصف</label>
|
||||
<input type="text" name="description" id="edit_description" class="form-control" required>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label>حساب المدين</label>
|
||||
<select name="debit_account" id="edit_debit_account" class="form-select accountSelectEdit" required style="width: 100%;">
|
||||
<option value="" disabled>اختر الحساب...</option>
|
||||
<?php foreach (get_all_accounts() as $acc): ?>
|
||||
<option value="<?= htmlspecialchars($acc['name']) ?>"><?= htmlspecialchars($acc['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label>حساب الدائن</label>
|
||||
<select name="credit_account" id="edit_credit_account" class="form-select accountSelectEdit" required style="width: 100%;">
|
||||
<option value="" disabled>اختر الحساب...</option>
|
||||
<?php foreach (get_all_accounts() as $acc): ?>
|
||||
<option value="<?= htmlspecialchars($acc['name']) ?>"><?= htmlspecialchars($acc['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label>المبلغ</label>
|
||||
<input type="number" step="0.01" name="amount" id="edit_amount" class="form-control" required min="0.01">
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3 class="text-right">دفتر الأستاذ (General Ledger)</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-bordered text-right align-middle table-sm">
|
||||
<thead class="table-light"><tr><th>التاريخ</th><th>الوصف</th><th>المرجع</th><th>الحساب</th><th>مدين</th><th>دائن</th><th>الإجراءات</th></tr></thead>
|
||||
<tbody>
|
||||
<?php if(empty($ledger)): ?>
|
||||
<tr><td colspan="7" class="text-center">لا توجد قيود.</td></tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($ledger as $row): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($row['date']) ?></td>
|
||||
<td><?= htmlspecialchars($row['description']) ?></td>
|
||||
<td><?= htmlspecialchars($row['reference']) ?></td>
|
||||
<td><?= htmlspecialchars($row['account_name']) ?></td>
|
||||
<td><?= number_format($row['debit'], 2) ?></td>
|
||||
<td><?= number_format($row['credit'], 2) ?></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-link text-warning p-0 me-2" title="تعديل" onclick="openEditModal(<?= $row['id'] ?>)"><i class="fas fa-edit"></i></button>
|
||||
|
||||
<form method="POST" class="d-inline delete-form">
|
||||
<input type="hidden" name="delete_entry" value="1">
|
||||
<input type="hidden" name="delete_id" value="<?= htmlspecialchars($row['id']) ?>">
|
||||
<button type="button" class="btn btn-sm btn-link text-danger p-0 delete-btn" title="حذف">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php renderPagination($page, $totalFiltered, $limit); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.accountSelect').select2({
|
||||
theme: 'bootstrap-5',
|
||||
dir: 'rtl',
|
||||
dropdownParent: $('#journalModal')
|
||||
});
|
||||
$('.accountSelectEdit').select2({
|
||||
theme: 'bootstrap-5',
|
||||
dir: 'rtl',
|
||||
dropdownParent: $('#editJournalModal')
|
||||
});
|
||||
|
||||
<?php if (isset($message)): ?>
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'نجاح!',
|
||||
text: '<?= addslashes($message) ?>',
|
||||
confirmButtonColor: '#3085d6',
|
||||
confirmButtonText: 'حسناً'
|
||||
});
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'خطأ!',
|
||||
text: '<?= addslashes($error) ?>',
|
||||
confirmButtonColor: '#d33',
|
||||
confirmButtonText: 'حسناً'
|
||||
});
|
||||
<?php endif; ?>
|
||||
|
||||
$('.delete-btn').on('click', function() {
|
||||
var form = $(this).closest('form');
|
||||
Swal.fire({
|
||||
title: 'هل أنت متأكد؟',
|
||||
text: 'سيتم حذف القيد من جميع الحسابات المرتبطة بشكل نهائي.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
cancelButtonColor: '#6c757d',
|
||||
confirmButtonText: 'نعم، احذف!',
|
||||
cancelButtonText: 'إلغاء'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function openEditModal(id) {
|
||||
$.ajax({
|
||||
url: 'api/get_journal_entry.php?id=' + id,
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
var data = response.data;
|
||||
$('#edit_id').val(data.id);
|
||||
$('#edit_date').val(data.date);
|
||||
$('#edit_reference').val(data.reference);
|
||||
$('#edit_description').val(data.description);
|
||||
$('#edit_debit_account').val(data.debit_account).trigger('change');
|
||||
$('#edit_credit_account').val(data.credit_account).trigger('change');
|
||||
$('#edit_amount').val(data.amount);
|
||||
|
||||
var editModal = new bootstrap.Modal(document.getElementById('editJournalModal'));
|
||||
editModal.show();
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'خطأ',
|
||||
text: 'حدث خطأ أثناء جلب بيانات القيد.',
|
||||
confirmButtonColor: '#3085d6',
|
||||
confirmButtonText: 'حسناً'
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'خطأ',
|
||||
text: 'حدث خطأ في الاتصال بالخادم.',
|
||||
confirmButtonColor: '#3085d6',
|
||||
confirmButtonText: 'حسناً'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
196
accounting.php.bak
Normal file
196
accounting.php.bak
Normal file
@ -0,0 +1,196 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
require_once 'includes/header.php';
|
||||
require_once 'includes/accounting_functions.php';
|
||||
|
||||
// Check permission
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$stmt = db()->prepare("SELECT * FROM user_permissions WHERE user_id = ? AND page = 'accounting' AND can_view = 1");
|
||||
$stmt->execute([$user_id]);
|
||||
if (!$stmt->fetch()) {
|
||||
echo "<div class='container mt-4' dir='rtl'>لا تملك صلاحية الوصول لهذه الصفحة.</div>";
|
||||
require_once 'includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_entry'])) {
|
||||
$date = $_POST["date"] ?? "";
|
||||
$description = $_POST["description"] ?? "";
|
||||
$reference = $_POST["reference"] ?? "";
|
||||
$entries = [
|
||||
["account" => $_POST["debit_account"] ?? "", "debit" => (float)($_POST["amount"] ?? 0), "credit" => 0],
|
||||
["account" => $_POST["credit_account"] ?? "", "debit" => 0, "credit" => (float)($_POST["amount"] ?? 0)]
|
||||
];
|
||||
|
||||
if (add_journal_entry($date, $description, $reference, $entries)) {
|
||||
$message = "تم إضافة القيد بنجاح.";
|
||||
} else {
|
||||
$error = "حدث خطأ أثناء إضافة القيد.";
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination and Filtering setup
|
||||
$page = isset($_GET['p']) ? (int)$_GET['p'] : 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
$search = $_GET['search'] ?? '';
|
||||
$date_from = $_GET['date_from'] ?? '';
|
||||
$date_to = $_GET['date_to'] ?? '';
|
||||
|
||||
// Fetch ledger data with filters
|
||||
$ledger_all = get_full_ledger_filtered($search, $date_from, $date_to);
|
||||
$total_items = count($ledger_all);
|
||||
$total_pages = ceil($total_items / $limit);
|
||||
$ledger = array_slice($ledger_all, $offset, $limit);
|
||||
?>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<style>
|
||||
.table td, .table th { padding: 0.3rem 0.5rem; }
|
||||
.action-icon { cursor: pointer; text-decoration: none; }
|
||||
.action-icon:hover { opacity: 0.7; }
|
||||
</style>
|
||||
|
||||
<div class="container mt-4" dir="rtl">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="text-right">المحاسبة (Accounting)</h2>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#journalModal">
|
||||
<i class="fas fa-plus"></i> إضافة قيد جديد
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (isset($message)) echo "<div class='alert alert-success'>$message</div>"; ?>
|
||||
<?php if (isset($error)) echo "<div class='alert alert-danger'>$error</div>"; ?>
|
||||
|
||||
<!-- Filter Form -->
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-body py-2">
|
||||
<form method="GET" class="row g-2 align-items-center">
|
||||
<div class="col-md-5">
|
||||
<input type="text" name="search" class="form-control form-control-sm" placeholder="بحث..." value="<?= htmlspecialchars($search) ?>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" name="date_from" class="form-control form-control-sm" value="<?= htmlspecialchars($date_from) ?>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" name="date_to" class="form-control form-control-sm" value="<?= htmlspecialchars($date_to) ?>">
|
||||
</div>
|
||||
<div class="col-md-3 text-end">
|
||||
<button type="submit" class="btn btn-sm btn-secondary"><i class="fas fa-filter"></i> تصفية</button>
|
||||
<a href="accounting.php" class="btn btn-sm btn-outline-secondary"><i class="fas fa-sync"></i></a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Journal Modal -->
|
||||
<div class="modal fade" id="journalModal" tabindex="-1" aria-labelledby="journalModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form method="POST">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="journalModalLabel">إضافة قيد محاسبي</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="add_entry" value="1">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label>التاريخ</label>
|
||||
<input type="date" name="date" class="form-control" required value="<?= date('Y-m-d') ?>">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label>المرجع</label>
|
||||
<input type="text" name="reference" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label>الوصف</label>
|
||||
<input type="text" name="description" class="form-control" required>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label>حساب المدين</label>
|
||||
<select name="debit_account" class="form-select accountSelect" required style="width: 100%;">
|
||||
<?php foreach (get_all_accounts() as $acc): ?>
|
||||
<option value="<?= htmlspecialchars($acc['name']) ?>"><?= htmlspecialchars($acc['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label>حساب الدائن</label>
|
||||
<select name="credit_account" class="form-select accountSelect" required style="width: 100%;">
|
||||
<?php foreach (get_all_accounts() as $acc): ?>
|
||||
<option value="<?= htmlspecialchars($acc['name']) ?>"><?= htmlspecialchars($acc['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label>المبلغ</label>
|
||||
<input type="number" step="0.01" name="amount" class="form-control" required min="0.01">
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3 class="text-right">دفتر الأستاذ (General Ledger)</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-bordered text-right align-middle table-sm">
|
||||
<thead class="table-light"><tr><th>التاريخ</th><th>الوصف</th><th>المرجع</th><th>الحساب</th><th>مدين</th><th>دائن</th><th>الإجراءات</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($ledger as $row): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($row['date']) ?></td>
|
||||
<td><?= htmlspecialchars($row['description']) ?></td>
|
||||
<td><?= htmlspecialchars($row['reference']) ?></td>
|
||||
<td><?= htmlspecialchars($row['account_name']) ?></td>
|
||||
<td><?= number_format($row['debit'], 2) ?></td>
|
||||
<td><?= number_format($row['credit'], 2) ?></td>
|
||||
<td>
|
||||
<a href="#" class="action-icon text-warning me-2" title="تعديل" onclick="alert('تعديل القيد <?= $row['id'] ?>')"><i class="fas fa-edit"></i></a>
|
||||
<a href="#" class="action-icon text-danger" title="حذف" onclick="if(confirm('هل أنت متأكد؟')) alert('حذف القيد <?= $row['id'] ?>')"><i class="fas fa-trash"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm justify-content-center">
|
||||
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
|
||||
<li class="page-item <?= $i == $page ? 'active' : '' ?>">
|
||||
<a class="page-link" href="?p=<?= $i ?>&search=<?= urlencode($search) ?>&date_from=<?= urlencode($date_from) ?>&date_to=<?= urlencode($date_to) ?>"><?= $i ?></a>
|
||||
</li>
|
||||
<?php endfor; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.accountSelect').select2({
|
||||
theme: 'bootstrap-5',
|
||||
dir: 'rtl',
|
||||
dropdownParent: $('#journalModal')
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
2
accounting_temp.php
Normal file
2
accounting_temp.php
Normal file
@ -0,0 +1,2 @@
|
||||
<span class="my-custom-action text-warning me-2" title="تعديل" onclick='alert("تعديل القيد " + <?= json_encode($row["id"]) ?>)'><i class="fas fa-edit"></i></span>
|
||||
<span class="my-custom-action text-danger" title="حذف" onclick='if(confirm("هل أنت متأكد؟")) alert("حذف القيد " + <?= json_encode($row["id"]) ?>)'><i class="fas fa-trash"></i></span>
|
||||
240
accounts.php
Normal file
240
accounts.php
Normal file
@ -0,0 +1,240 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
require_once 'includes/header.php';
|
||||
require_once 'includes/accounting_functions.php';
|
||||
|
||||
// Check permission
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$stmt = db()->prepare("SELECT * FROM user_permissions WHERE user_id = ? AND page = 'accounting' AND can_view = 1");
|
||||
$stmt->execute([$user_id]);
|
||||
if (!$stmt->fetch()) {
|
||||
echo "<div class='container mt-4' dir='rtl'>لا تملك صلاحية الوصول لهذه الصفحة.</div>";
|
||||
require_once 'includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$message = null;
|
||||
$messageType = 'success';
|
||||
|
||||
// Handle form submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_POST['add_account'])) {
|
||||
$name = $_POST['name'];
|
||||
$type = $_POST['type'];
|
||||
$stmt = db()->prepare("INSERT INTO accounting_accounts (name, type) VALUES (?, ?)");
|
||||
if($stmt->execute([$name, $type])) {
|
||||
$message = "تم إضافة الحساب بنجاح.";
|
||||
}
|
||||
} elseif (isset($_POST['delete_account'])) {
|
||||
$id = $_POST['id'];
|
||||
$stmt = db()->prepare("DELETE FROM accounting_accounts WHERE id = ?");
|
||||
if($stmt->execute([$id])) {
|
||||
$message = "تم حذف الحساب.";
|
||||
}
|
||||
} elseif (isset($_POST['edit_account'])) {
|
||||
$id = $_POST['id'];
|
||||
$name = $_POST['name'];
|
||||
$type = $_POST['type'];
|
||||
$stmt = db()->prepare("UPDATE accounting_accounts SET name = ?, type = ? WHERE id = ?");
|
||||
if($stmt->execute([$name, $type, $id])) {
|
||||
$message = "تم تحديث الحساب بنجاح.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination
|
||||
$page = isset($_GET['p']) ? (int)$_GET['p'] : 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$totalAccounts = db()->query("SELECT COUNT(*) FROM accounting_accounts")->fetchColumn();
|
||||
$totalPages = ceil($totalAccounts / $limit);
|
||||
|
||||
$accounts = db()->prepare("SELECT * FROM accounting_accounts ORDER BY type, name LIMIT ? OFFSET ?");
|
||||
$accounts->bindValue(1, $limit, PDO::PARAM_INT);
|
||||
$accounts->bindValue(2, $offset, PDO::PARAM_INT);
|
||||
$accounts->execute();
|
||||
$accounts = $accounts->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Map English types to Arabic
|
||||
$typeMap = [
|
||||
'Assets' => 'أصول',
|
||||
'Liabilities' => 'خصوم',
|
||||
'Equity' => 'حقوق ملكية',
|
||||
'Revenue' => 'إيرادات',
|
||||
'Expenses' => 'مصروفات',
|
||||
'أصول' => 'أصول',
|
||||
'خصوم' => 'خصوم',
|
||||
'حقوق ملكية' => 'حقوق ملكية',
|
||||
'إيرادات' => 'إيرادات',
|
||||
'مصروفات' => 'مصروفات'
|
||||
];
|
||||
?>
|
||||
|
||||
<!-- SweetAlert2 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<style>
|
||||
/* Reduce row height for a more compact table */
|
||||
.table-compact td, .table-compact th {
|
||||
padding: 0.4rem 0.6rem;
|
||||
vertical-align: middle;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.table-compact .btn-sm {
|
||||
padding: 0.2rem 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container mt-4" dir="rtl">
|
||||
<h2 class="text-right mb-4">دليل الحسابات</h2>
|
||||
|
||||
<div class="card mb-4 shadow-sm border-0">
|
||||
<div class="card-header bg-primary text-white">إضافة/تعديل حساب</div>
|
||||
<div class="card-body bg-light">
|
||||
<form method="POST" id="accountForm">
|
||||
<input type="hidden" name="add_account" value="1" id="formAction">
|
||||
<input type="hidden" name="id" id="editId">
|
||||
<div class="row">
|
||||
<div class="col-md-5 mb-2">
|
||||
<label class="font-weight-bold">اسم الحساب</label>
|
||||
<input type="text" name="name" class="form-control" id="editName" required>
|
||||
</div>
|
||||
<div class="col-md-5 mb-2">
|
||||
<label class="font-weight-bold">نوع الحساب</label>
|
||||
<select name="type" class="form-control" id="editType" required>
|
||||
<option value="أصول">أصول</option>
|
||||
<option value="خصوم">خصوم</option>
|
||||
<option value="حقوق ملكية">حقوق ملكية</option>
|
||||
<option value="إيرادات">إيرادات</option>
|
||||
<option value="مصروفات">مصروفات</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end mb-2">
|
||||
<button type="submit" class="btn btn-primary w-100 shadow-sm" id="formButton">
|
||||
<i class="fas fa-plus"></i> إضافة
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped table-hover table-compact text-right mb-0">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th>الاسم</th>
|
||||
<th>النوع</th>
|
||||
<th style="width: 120px; text-align: center;">إجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($accounts as $account): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($account['name']) ?></td>
|
||||
<td>
|
||||
<span class="badge badge-info px-2 py-1" style="color: black;">
|
||||
<?= htmlspecialchars($typeMap[$account['type']] ?? $account['type']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button class="btn btn-warning text-white btn-sm shadow-sm mx-1" onclick="editAccount(<?= $account['id'] ?>, '<?= htmlspecialchars($account['name'], ENT_QUOTES) ?>', '<?= htmlspecialchars($account['type'], ENT_QUOTES) ?>')" title="تعديل">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</button>
|
||||
<form method="POST" style="display:inline;" class="delete-form">
|
||||
<input type="hidden" name="delete_account" value="1">
|
||||
<input type="hidden" name="id" value="<?= $account['id'] ?>">
|
||||
<button type="button" class="btn btn-danger btn-sm shadow-sm mx-1" onclick="confirmDelete(this)" title="حذف">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($accounts)): ?>
|
||||
<tr>
|
||||
<td colspan="3" class="text-center py-3 text-muted">لا توجد حسابات مضافة بعد.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<nav class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
|
||||
<li class="page-item <?= $i == $page ? 'active' : '' ?>">
|
||||
<a class="page-link" href="?p=<?= $i ?>"><?= $i ?></a>
|
||||
</li>
|
||||
<?php endfor; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// SweetAlert Success Message from PHP
|
||||
<?php if ($message): ?>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'نجاح!',
|
||||
text: '<?= htmlspecialchars($message) ?>',
|
||||
confirmButtonText: 'حسناً',
|
||||
confirmButtonColor: '#28a745',
|
||||
timer: 3000,
|
||||
timerProgressBar: true
|
||||
});
|
||||
});
|
||||
<?php endif; ?>
|
||||
|
||||
function editAccount(id, name, type) {
|
||||
document.getElementById('formAction').name = 'edit_account';
|
||||
document.getElementById('editId').value = id;
|
||||
document.getElementById('editName').value = name;
|
||||
document.getElementById('editType').value = type;
|
||||
|
||||
const formBtn = document.getElementById('formButton');
|
||||
formBtn.innerHTML = '<i class="fas fa-save"></i> حفظ التعديلات';
|
||||
formBtn.classList.remove('btn-primary');
|
||||
formBtn.classList.add('btn-success');
|
||||
|
||||
// Animate scroll to form
|
||||
window.scrollTo({top: 0, behavior: 'smooth'});
|
||||
|
||||
// Highlight form briefly to show it's in edit mode
|
||||
const cardBody = document.querySelector('.card-body');
|
||||
cardBody.style.transition = 'background-color 0.5s';
|
||||
cardBody.style.backgroundColor = '#fff3cd'; // warning light
|
||||
setTimeout(() => {
|
||||
cardBody.style.backgroundColor = '';
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function confirmDelete(button) {
|
||||
Swal.fire({
|
||||
title: 'هل أنت متأكد؟',
|
||||
text: "لن تتمكن من استعادة هذا الحساب لاحقاً!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#dc3545',
|
||||
cancelButtonColor: '#6c757d',
|
||||
confirmButtonText: '<i class="fas fa-trash-alt"></i> نعم، احذفه!',
|
||||
cancelButtonText: 'إلغاء',
|
||||
reverseButtons: true
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
button.closest('.delete-form').submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
29
api/chat.php
29
api/chat.php
@ -7,7 +7,7 @@ $input = json_decode(file_get_contents('php://input'), true);
|
||||
$message = $input['message'] ?? '';
|
||||
|
||||
if (empty($message)) {
|
||||
echo json_encode(['reply' => "I didn't catch that. Could you repeat?"]);
|
||||
echo json_encode(['reply' => "لم أفهم ذلك. هل يمكنك التكرار؟"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
@ -16,18 +16,18 @@ try {
|
||||
$stmt = db()->query("SELECT keywords, answer FROM faqs");
|
||||
$faqs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$knowledgeBase = "Here is the knowledge base for this website:\n\n";
|
||||
$knowledgeBase = "إليك قاعدة المعرفة لهذا الموقع:\n\n";
|
||||
foreach ($faqs as $faq) {
|
||||
$knowledgeBase .= "Q: " . $faq['keywords'] . "\nA: " . $faq['answer'] . "\n---\n";
|
||||
$knowledgeBase .= "س: " . $faq['keywords'] . "\nج: " . $faq['answer'] . "\n---\n";
|
||||
}
|
||||
|
||||
// 2. Construct Prompt for AI
|
||||
$systemPrompt = "You are a helpful, friendly AI assistant for this website. " .
|
||||
"Use the provided Knowledge Base to answer user questions accurately. " .
|
||||
"If the answer is found in the Knowledge Base, rephrase it naturally. " .
|
||||
"If the answer is NOT in the Knowledge Base, use your general knowledge to help, " .
|
||||
"but politely mention that you don't have specific information about that if it seems like a site-specific question. " .
|
||||
"Keep answers concise and professional.\n\n" .
|
||||
$systemPrompt = "أنت مساعد ذكاء اصطناعي مفيد وودود لهذا الموقع. " .
|
||||
"استخدم قاعدة المعرفة المقدمة للإجابة على أسئلة المستخدمين بدقة. " .
|
||||
"إذا كانت الإجابة موجودة في قاعدة المعرفة، فقم بصياغتها بشكل طبيعي. " .
|
||||
"إذا لم تكن الإجابة في قاعدة المعرفة، فاستخدم معرفتك العامة للمساعدة، " .
|
||||
"ولكن اذكر بأدب أنك ليس لديك معلومات محددة حول ذلك إذا كان يبدو سؤالاً خاصاً بالموقع. " .
|
||||
"اجعل الإجابات موجزة واحترافية وباللغة العربية.\n\n" .
|
||||
$knowledgeBase;
|
||||
|
||||
// 3. Call AI API
|
||||
@ -40,7 +40,12 @@ try {
|
||||
]);
|
||||
|
||||
if (!empty($response['success'])) {
|
||||
$aiReply = LocalAIApi::extractText($response);
|
||||
$text = LocalAIApi::extractText($response);
|
||||
if ($text === '') {
|
||||
$decoded = LocalAIApi::decodeJsonFromResponse($response);
|
||||
$text = $decoded ? json_encode($decoded, JSON_UNESCAPED_UNICODE) : (string)($response['data'] ?? '');
|
||||
}
|
||||
$aiReply = $text;
|
||||
|
||||
// 4. Save to Database
|
||||
try {
|
||||
@ -55,10 +60,10 @@ try {
|
||||
} else {
|
||||
// Fallback if AI fails
|
||||
error_log("AI Error: " . ($response['error'] ?? 'Unknown'));
|
||||
echo json_encode(['reply' => "I'm having trouble connecting to my brain right now. Please try again later."]);
|
||||
echo json_encode(['reply' => "أواجه مشكلة في الاتصال بذكائي الآن. يرجى المحاولة مرة أخرى لاحقاً."]);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Chat Error: " . $e->getMessage());
|
||||
echo json_encode(['reply' => "An internal error occurred."]);
|
||||
echo json_encode(['reply' => "حدث خطأ داخلي."]);
|
||||
}
|
||||
15
api/get_journal_entry.php
Normal file
15
api/get_journal_entry.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
require_once __DIR__ . '/../includes/accounting_functions.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
if ($id > 0) {
|
||||
$data = get_journal_entry_details($id);
|
||||
if ($data) {
|
||||
echo json_encode(['success' => true, 'data' => $data]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
echo json_encode(['success' => false, 'error' => 'Entry not found.']);
|
||||
26
api/update_theme.php
Normal file
26
api/update_theme.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
echo json_encode(['success' => false, 'error' => 'غير مصرح لك بالوصول']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$theme = $data['theme'] ?? 'light';
|
||||
|
||||
// Validate theme
|
||||
$allowed_themes = ['light', 'dark', 'midnight', 'forest', 'ocean', 'sunset', 'royal'];
|
||||
if (!in_array($theme, $allowed_themes)) {
|
||||
echo json_encode(['success' => false, 'error' => 'مظهر غير صالح']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = db()->prepare("UPDATE users SET theme = ? WHERE id = ?");
|
||||
$stmt->execute([$theme, $_SESSION['user_id']]);
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@ -1,14 +1,204 @@
|
||||
body {
|
||||
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient 15s ease infinite;
|
||||
color: #212529;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background-color: var(--bs-body-bg);
|
||||
color: var(--bs-body-color);
|
||||
font-family: 'Cairo', 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Fix RTL spacing */
|
||||
.me-2 { margin-left: 0.5rem !important; margin-right: 0 !important; }
|
||||
.ms-auto { margin-right: auto !important; margin-left: 0 !important; }
|
||||
|
||||
/*
|
||||
THEME DEFINITIONS
|
||||
We leverage Bootstrap 5 CSS variables to create different themes.
|
||||
Light and Dark are built-in, but we can tweak them too if we want.
|
||||
*/
|
||||
|
||||
/* 1. Midnight Theme (Deep Blue) */
|
||||
[data-bs-theme="midnight"] {
|
||||
--bs-body-bg: #0f172a;
|
||||
--bs-body-color: #f1f5f9;
|
||||
--bs-heading-color: #f1f5f9;
|
||||
--bs-body-bg-rgb: 15, 23, 42;
|
||||
--bs-tertiary-bg: #1e293b;
|
||||
--bs-secondary-bg: #334155;
|
||||
--bs-border-color: #334155;
|
||||
--bs-link-color: #60a5fa;
|
||||
--bs-link-hover-color: #93c5fd;
|
||||
--bs-card-bg: #1e293b;
|
||||
--bs-card-border-color: #334155;
|
||||
}
|
||||
|
||||
/* 2. Forest Theme (Deep Green) */
|
||||
[data-bs-theme="forest"] {
|
||||
--bs-body-bg: #052e16;
|
||||
--bs-body-color: #ecfdf5;
|
||||
--bs-heading-color: #ecfdf5;
|
||||
--bs-body-bg-rgb: 5, 46, 22;
|
||||
--bs-tertiary-bg: #064e3b;
|
||||
--bs-secondary-bg: #065f46;
|
||||
--bs-border-color: #047857;
|
||||
--bs-link-color: #34d399;
|
||||
--bs-link-hover-color: #6ee7b7;
|
||||
--bs-card-bg: #064e3b;
|
||||
--bs-card-border-color: #047857;
|
||||
}
|
||||
|
||||
/* 3. Ocean Theme (Deep Teal/Blue) */
|
||||
[data-bs-theme="ocean"] {
|
||||
--bs-body-bg: #0c4a6e;
|
||||
--bs-body-color: #f0f9ff;
|
||||
--bs-heading-color: #f0f9ff;
|
||||
--bs-body-bg-rgb: 12, 74, 110;
|
||||
--bs-tertiary-bg: #075985;
|
||||
--bs-secondary-bg: #0369a1;
|
||||
--bs-border-color: #0284c7;
|
||||
--bs-link-color: #38bdf8;
|
||||
--bs-link-hover-color: #7dd3fc;
|
||||
--bs-card-bg: #075985;
|
||||
--bs-card-border-color: #0284c7;
|
||||
}
|
||||
|
||||
/* 4. Sunset Theme (Warm Dark) */
|
||||
[data-bs-theme="sunset"] {
|
||||
--bs-body-bg: #431407;
|
||||
--bs-body-color: #fff7ed;
|
||||
--bs-heading-color: #fff7ed;
|
||||
--bs-body-bg-rgb: 67, 20, 7;
|
||||
--bs-tertiary-bg: #7c2d12;
|
||||
--bs-secondary-bg: #9a3412;
|
||||
--bs-border-color: #c2410c;
|
||||
--bs-link-color: #fdba74;
|
||||
--bs-link-hover-color: #ffedd5;
|
||||
--bs-card-bg: #7c2d12;
|
||||
--bs-card-border-color: #c2410c;
|
||||
}
|
||||
|
||||
/* 5. Royal Theme (Deep Purple) */
|
||||
[data-bs-theme="royal"] {
|
||||
--bs-body-bg: #2e1065;
|
||||
--bs-body-color: #faf5ff;
|
||||
--bs-heading-color: #faf5ff;
|
||||
--bs-body-bg-rgb: 46, 16, 101;
|
||||
--bs-tertiary-bg: #4c1d95;
|
||||
--bs-secondary-bg: #5b21b6;
|
||||
--bs-border-color: #6d28d9;
|
||||
--bs-link-color: #c084fc;
|
||||
--bs-link-hover-color: #e9d5ff;
|
||||
--bs-card-bg: #4c1d95;
|
||||
--bs-card-border-color: #6d28d9;
|
||||
}
|
||||
|
||||
/*
|
||||
Shared overrides for ALL dark themes
|
||||
This ensures standard Bootstrap classes like .text-dark don't cause visibility issues.
|
||||
*/
|
||||
[data-bs-theme="midnight"],
|
||||
[data-bs-theme="forest"],
|
||||
[data-bs-theme="ocean"],
|
||||
[data-bs-theme="sunset"],
|
||||
[data-bs-theme="royal"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Text color overrides */
|
||||
[data-bs-theme="midnight"] .text-dark,
|
||||
[data-bs-theme="forest"] .text-dark,
|
||||
[data-bs-theme="ocean"] .text-dark,
|
||||
[data-bs-theme="sunset"] .text-dark,
|
||||
[data-bs-theme="royal"] .text-dark {
|
||||
color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="midnight"] .text-muted,
|
||||
[data-bs-theme="forest"] .text-muted,
|
||||
[data-bs-theme="ocean"] .text-muted,
|
||||
[data-bs-theme="sunset"] .text-muted,
|
||||
[data-bs-theme="royal"] .text-muted {
|
||||
color: rgba(255, 255, 255, 0.6) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="midnight"] .text-secondary,
|
||||
[data-bs-theme="forest"] .text-secondary,
|
||||
[data-bs-theme="ocean"] .text-secondary,
|
||||
[data-bs-theme="sunset"] .text-secondary,
|
||||
[data-bs-theme="royal"] .text-secondary {
|
||||
color: rgba(255, 255, 255, 0.75) !important;
|
||||
}
|
||||
|
||||
/* Background overrides for hardcoded bg-light/bg-white classes */
|
||||
[data-bs-theme="midnight"] .bg-light,
|
||||
[data-bs-theme="forest"] .bg-light,
|
||||
[data-bs-theme="ocean"] .bg-light,
|
||||
[data-bs-theme="sunset"] .bg-light,
|
||||
[data-bs-theme="royal"] .bg-light {
|
||||
background-color: var(--bs-tertiary-bg) !important;
|
||||
color: var(--bs-body-color) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="midnight"] .bg-white,
|
||||
[data-bs-theme="forest"] .bg-white,
|
||||
[data-bs-theme="ocean"] .bg-white,
|
||||
[data-bs-theme="sunset"] .bg-white,
|
||||
[data-bs-theme="royal"] .bg-white {
|
||||
background-color: var(--bs-card-bg) !important;
|
||||
color: var(--bs-body-color) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="midnight"] .table th,
|
||||
[data-bs-theme="forest"] .table th,
|
||||
[data-bs-theme="ocean"] .table th,
|
||||
[data-bs-theme="sunset"] .table th,
|
||||
[data-bs-theme="royal"] .table th {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
|
||||
/* Ensure cards and tables inherit theme colors correctly */
|
||||
[data-bs-theme="midnight"] .card,
|
||||
[data-bs-theme="forest"] .card,
|
||||
[data-bs-theme="ocean"] .card,
|
||||
[data-bs-theme="sunset"] .card,
|
||||
[data-bs-theme="royal"] .card {
|
||||
background-color: var(--bs-card-bg);
|
||||
border-color: var(--bs-card-border-color);
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
[data-bs-theme="midnight"] .table,
|
||||
[data-bs-theme="forest"] .table,
|
||||
[data-bs-theme="ocean"] .table,
|
||||
[data-bs-theme="sunset"] .table,
|
||||
[data-bs-theme="royal"] .table {
|
||||
color: var(--bs-body-color);
|
||||
--bs-table-color: var(--bs-body-color);
|
||||
--bs-table-bg: transparent;
|
||||
--bs-table-border-color: var(--bs-border-color);
|
||||
--bs-table-striped-bg: rgba(255, 255, 255, 0.05);
|
||||
--bs-table-hover-bg: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
[data-bs-theme="midnight"] .modal-content,
|
||||
[data-bs-theme="forest"] .modal-content,
|
||||
[data-bs-theme="ocean"] .modal-content,
|
||||
[data-bs-theme="sunset"] .modal-content,
|
||||
[data-bs-theme="royal"] .modal-content {
|
||||
background-color: var(--bs-tertiary-bg);
|
||||
border-color: var(--bs-border-color);
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
/* Sidebar overrides for specific themes */
|
||||
[data-bs-theme="midnight"] .sidebar { background-color: #1e293b !important; border-left: 1px solid #334155; }
|
||||
[data-bs-theme="forest"] .sidebar { background-color: #064e3b !important; border-left: 1px solid #065f46; }
|
||||
[data-bs-theme="ocean"] .sidebar { background-color: #075985 !important; border-left: 1px solid #0369a1; }
|
||||
[data-bs-theme="sunset"] .sidebar { background-color: #7c2d12 !important; border-left: 1px solid #9a3412; }
|
||||
[data-bs-theme="royal"] .sidebar { background-color: #4c1d95 !important; border-left: 1px solid #5b21b6; }
|
||||
|
||||
|
||||
.main-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -36,27 +226,26 @@ body {
|
||||
.chat-container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
background: var(--bs-body-bg);
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 85vh;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-bottom: 1px solid var(--bs-border-color);
|
||||
background: var(--bs-tertiary-bg);
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
@ -78,12 +267,21 @@ body {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] ::-webkit-scrollbar-thumb,
|
||||
[data-bs-theme="midnight"] ::-webkit-scrollbar-thumb,
|
||||
[data-bs-theme="forest"] ::-webkit-scrollbar-thumb,
|
||||
[data-bs-theme="ocean"] ::-webkit-scrollbar-thumb,
|
||||
[data-bs-theme="sunset"] ::-webkit-scrollbar-thumb,
|
||||
[data-bs-theme="royal"] ::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.message {
|
||||
@ -92,33 +290,34 @@ body {
|
||||
border-radius: 16px;
|
||||
line-height: 1.5;
|
||||
font-size: 0.95rem;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
||||
animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px) scale(0.95); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.message.visitor {
|
||||
align-self: flex-end;
|
||||
background: linear-gradient(135deg, #212529 0%, #343a40 100%);
|
||||
background: #0d6efd;
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.message.bot {
|
||||
align-self: flex-start;
|
||||
background: #ffffff;
|
||||
color: #212529;
|
||||
background: var(--bs-tertiary-bg); /* Use theme bg */
|
||||
color: var(--bs-body-color);
|
||||
border-bottom-left-radius: 4px;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
padding: 1.25rem;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
background: var(--bs-body-bg);
|
||||
border-top: 1px solid var(--bs-border-color);
|
||||
}
|
||||
|
||||
.chat-input-area form {
|
||||
@ -128,21 +327,22 @@ body {
|
||||
|
||||
.chat-input-area input {
|
||||
flex: 1;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem 1rem;
|
||||
outline: none;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
background: var(--bs-body-bg);
|
||||
color: var(--bs-body-color);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chat-input-area input:focus {
|
||||
border-color: #23a6d5;
|
||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
|
||||
border-color: #0d6efd;
|
||||
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.1);
|
||||
}
|
||||
|
||||
.chat-input-area button {
|
||||
background: #212529;
|
||||
background: #0d6efd;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
@ -153,98 +353,43 @@ body {
|
||||
}
|
||||
|
||||
.chat-input-area button:hover {
|
||||
background: #000;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Background Animations */
|
||||
.bg-animations {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blob {
|
||||
position: absolute;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
|
||||
}
|
||||
|
||||
.blob-1 {
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
background: rgba(238, 119, 82, 0.4);
|
||||
}
|
||||
|
||||
.blob-2 {
|
||||
bottom: -10%;
|
||||
right: -10%;
|
||||
background: rgba(35, 166, 213, 0.4);
|
||||
animation-delay: -7s;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
.blob-3 {
|
||||
top: 40%;
|
||||
left: 30%;
|
||||
background: rgba(231, 60, 126, 0.3);
|
||||
animation-delay: -14s;
|
||||
width: 450px;
|
||||
height: 450px;
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
|
||||
33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); }
|
||||
66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); }
|
||||
100% { transform: translate(0, 0) rotate(360deg) scale(1); }
|
||||
background: #0b5ed7;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.admin-link {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
color: #6c757d;
|
||||
text-decoration: none;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.admin-link:hover {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Admin Styles */
|
||||
.admin-container {
|
||||
max-width: 900px;
|
||||
margin: 3rem auto;
|
||||
padding: 2.5rem;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 20px 50px rgba(0,0,0,0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
background: var(--bs-body-bg);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 5px 20px rgba(0,0,0,0.05);
|
||||
border: 1px solid var(--bs-border-color);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.admin-container h1 {
|
||||
margin-top: 0;
|
||||
color: #212529;
|
||||
font-weight: 800;
|
||||
color: var(--bs-heading-color, var(--bs-body-color)); /* Use variable or fallback */
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.table {
|
||||
@ -252,13 +397,14 @@ body {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 8px;
|
||||
margin-top: 1.5rem;
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 1rem;
|
||||
color: #6c757d;
|
||||
color: #6c757d; /* Keep subtle for headers, but overridden for dark themes */
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
@ -266,13 +412,15 @@ body {
|
||||
}
|
||||
|
||||
.table td {
|
||||
background: #fff;
|
||||
background: var(--bs-body-bg);
|
||||
padding: 1rem;
|
||||
border: none;
|
||||
border-top: 1px solid var(--bs-border-color);
|
||||
border-bottom: 1px solid var(--bs-border-color);
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.table tr td:first-child { border-radius: 12px 0 0 12px; }
|
||||
.table tr td:last-child { border-radius: 0 12px 12px 0; }
|
||||
.table tr td:first-child { border-left: 1px solid var(--bs-border-color); border-radius: 8px 0 0 8px; }
|
||||
.table tr td:last-child { border-right: 1px solid var(--bs-border-color); border-radius: 0 8px 8px 0; }
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
@ -283,20 +431,94 @@ body {
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bs-body-bg);
|
||||
color: var(--bs-body-color);
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #23a6d5;
|
||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
|
||||
border-color: #0d6efd;
|
||||
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.1);
|
||||
background: var(--bs-body-bg);
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
/* Compact Table Class */
|
||||
.table-compact td, .table-compact th {
|
||||
padding: 0.5rem 0.75rem !important;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.table-compact tr td:first-child { border-radius: 4px 0 0 4px; }
|
||||
.table-compact tr td:last-child { border-radius: 0 4px 4px 0; }
|
||||
|
||||
/* ---------------------------------------------------------
|
||||
NEW SIDEBAR STYLES (Collapsible Groups)
|
||||
--------------------------------------------------------- */
|
||||
.sidebar-group-btn {
|
||||
width: 100%;
|
||||
text-align: right; /* RTL */
|
||||
padding: 12px 20px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgba(255,255,255,0.7);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: all 0.2s;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.sidebar-group-btn:hover {
|
||||
color: #fff;
|
||||
background-color: rgba(255,255,255,0.05);
|
||||
}
|
||||
.sidebar-group-btn[aria-expanded="true"] {
|
||||
background-color: rgba(255,255,255,0.05);
|
||||
color: #fff;
|
||||
}
|
||||
.sidebar-group-btn .group-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.sidebar-group-btn .arrow-icon {
|
||||
font-size: 0.8em;
|
||||
transition: transform 0.3s;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.sidebar-group-btn[aria-expanded="true"] .arrow-icon {
|
||||
transform: rotate(-180deg); /* Adjust for RTL if needed, usually just flips vertically */
|
||||
}
|
||||
|
||||
/* Colors for groups (Text & Icon) */
|
||||
.group-mail { color: #ffca28 !important; } /* Amber */
|
||||
.group-acct { color: #42a5f5 !important; } /* Blue */
|
||||
.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 {
|
||||
padding-right: 45px; /* RTL indent */
|
||||
padding-left: 20px;
|
||||
font-size: 0.9em;
|
||||
color: rgba(255,255,255,0.6);
|
||||
}
|
||||
.sidebar .collapse .nav-link:hover,
|
||||
.sidebar .collapse .nav-link.active {
|
||||
color: #fff;
|
||||
}
|
||||
.group-meetings { color: #20c997 !important; } /* Teal */
|
||||
|
||||
@ -3,6 +3,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
|
||||
if (!chatForm || !chatInput || !chatMessages) {
|
||||
return; // Not on the chat page
|
||||
}
|
||||
|
||||
const appendMessage = (text, sender) => {
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.classList.add('message', sender);
|
||||
|
||||
47
balance_sheet.php
Normal file
47
balance_sheet.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
require_once 'includes/header.php';
|
||||
require_once 'includes/accounting_functions.php';
|
||||
|
||||
// Check permission
|
||||
if (!canView('accounting')) {
|
||||
echo "<div class='container mt-4' dir='rtl'>لا تملك صلاحية الوصول لهذه الصفحة.</div>";
|
||||
require_once 'includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$balance_sheet = get_balance_sheet();
|
||||
?>
|
||||
|
||||
<div class="container mt-4" dir="rtl">
|
||||
<h2 class="text-right">الميزانية العمومية (Balance Sheet)</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-primary text-white text-center mb-3">
|
||||
<div class="card-body">
|
||||
<h5>الأصول</h5>
|
||||
<h3><?= number_format($balance_sheet['أصول'], 3) ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-danger text-white text-center mb-3">
|
||||
<div class="card-body">
|
||||
<h5>الخصوم</h5>
|
||||
<h3><?= number_format($balance_sheet['خصوم'], 3) ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-success text-white text-center mb-3">
|
||||
<div class="card-body">
|
||||
<h5>حقوق الملكية</h5>
|
||||
<h3><?= number_format($balance_sheet['حقوق ملكية'], 3) ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
657
charity-settings.php
Normal file
657
charity-settings.php
Normal file
@ -0,0 +1,657 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
require_once __DIR__ . '/m_services/MailService.php';
|
||||
|
||||
// Only users with settings view permission can access this page
|
||||
if (!canView('settings')) {
|
||||
redirect("index.php");
|
||||
}
|
||||
|
||||
$success_msg = '';
|
||||
$error_msg = '';
|
||||
|
||||
// Handle Re-enable SMTP
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['enable_smtp'])) {
|
||||
if (canEdit('settings')) {
|
||||
db()->query("UPDATE smtp_settings SET is_enabled = 1, consecutive_failures = 0 WHERE id = 1");
|
||||
$_SESSION['success'] = 'تم إعادة تفعيل SMTP وتصفير عداد الأخطاء';
|
||||
} else {
|
||||
$_SESSION['error'] = 'عذراً، ليس لديك الصلاحية لتعديل الإعدادات';
|
||||
}
|
||||
redirect('charity-settings.php');
|
||||
}
|
||||
|
||||
// Fetch charity settings
|
||||
$stmt = db()->query("SELECT * FROM charity_settings WHERE id = 1");
|
||||
$charity = $stmt->fetch();
|
||||
|
||||
// Fetch SMTP settings
|
||||
$stmt = db()->query("SELECT * FROM smtp_settings WHERE id = 1");
|
||||
$smtp = $stmt->fetch();
|
||||
|
||||
// Handle Charity Settings Update
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_charity'])) {
|
||||
if (!canEdit('settings')) {
|
||||
$_SESSION['error'] = 'عذراً، ليس لديك الصلاحية لتعديل الإعدادات';
|
||||
} else {
|
||||
$charity_name = $_POST['charity_name'];
|
||||
$charity_slogan = $_POST['charity_slogan'] ?? null;
|
||||
$charity_email = $_POST['charity_email'];
|
||||
$charity_phone = $_POST['charity_phone'];
|
||||
$charity_address = $_POST['charity_address'];
|
||||
$charity_logo = $charity['charity_logo'];
|
||||
$charity_favicon = $charity['charity_favicon'];
|
||||
|
||||
$upload_dir = 'uploads/charity/';
|
||||
if (!is_dir($upload_dir)) mkdir($upload_dir, 0775, true);
|
||||
|
||||
if (isset($_FILES['charity_logo']) && $_FILES['charity_logo']['error'] === UPLOAD_ERR_OK) {
|
||||
$file_ext = pathinfo($_FILES['charity_logo']['name'], PATHINFO_EXTENSION);
|
||||
$new_logo = 'logo_' . time() . '.' . $file_ext;
|
||||
if (move_uploaded_file($_FILES['charity_logo']['tmp_name'], $upload_dir . $new_logo)) {
|
||||
$charity_logo = $upload_dir . $new_logo;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_FILES['charity_favicon']) && $_FILES['charity_favicon']['error'] === UPLOAD_ERR_OK) {
|
||||
$file_ext = pathinfo($_FILES['charity_favicon']['name'], PATHINFO_EXTENSION);
|
||||
$new_favicon = 'favicon_' . time() . '.' . $file_ext;
|
||||
if (move_uploaded_file($_FILES['charity_favicon']['tmp_name'], $upload_dir . $new_favicon)) {
|
||||
$charity_favicon = $upload_dir . $new_favicon;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = db()->prepare("UPDATE charity_settings SET charity_name = ?, charity_slogan = ?, charity_email = ?, charity_phone = ?, charity_address = ?, charity_logo = ?, charity_favicon = ? WHERE id = 1");
|
||||
$stmt->execute([$charity_name, $charity_slogan, $charity_email, $charity_phone, $charity_address, $charity_logo, $charity_favicon]);
|
||||
$_SESSION['success'] = 'تم تحديث إعدادات النظام بنجاح';
|
||||
}
|
||||
redirect('charity-settings.php');
|
||||
}
|
||||
|
||||
// Handle Advanced Settings Update (Super Admin Only)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_advanced'])) {
|
||||
if (!isSuperAdmin()) {
|
||||
$_SESSION['error'] = 'عذراً، هذا الإجراء متاح للمدير الخارق فقط';
|
||||
} else {
|
||||
$site_maintenance = isset($_POST['site_maintenance']) ? 1 : 0;
|
||||
$allow_registration = isset($_POST['allow_registration']) ? 1 : 0;
|
||||
$site_footer = $_POST['site_footer'];
|
||||
|
||||
$stmt = db()->prepare("UPDATE charity_settings SET site_maintenance = ?, allow_registration = ?, site_footer = ? WHERE id = 1");
|
||||
$stmt->execute([$site_maintenance, $allow_registration, $site_footer]);
|
||||
$_SESSION['success'] = 'تم تحديث الإعدادات المتقدمة بنجاح';
|
||||
}
|
||||
redirect('charity-settings.php');
|
||||
}
|
||||
|
||||
// Handle SMTP Settings Update
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_smtp'])) {
|
||||
if (!canEdit('settings')) {
|
||||
$_SESSION['error'] = 'عذراً، ليس لديك الصلاحية لتعديل الإعدادات';
|
||||
} else {
|
||||
$stmt = db()->prepare("UPDATE smtp_settings SET smtp_host = ?, smtp_port = ?, smtp_secure = ?, smtp_user = ?, smtp_pass = ?, from_email = ?, from_name = ?, reply_to = ?, max_failures = ? WHERE id = 1");
|
||||
$stmt->execute([
|
||||
$_POST['smtp_host'],
|
||||
(int)$_POST['smtp_port'],
|
||||
$_POST['smtp_secure'],
|
||||
$_POST['smtp_user'],
|
||||
$_POST['smtp_pass'],
|
||||
$_POST['from_email'],
|
||||
$_POST['from_name'],
|
||||
$_POST['reply_to'],
|
||||
(int)$_POST['max_failures']
|
||||
]);
|
||||
$_SESSION['success'] = 'تم تحديث إعدادات البريد (SMTP) بنجاح';
|
||||
}
|
||||
redirect('charity-settings.php');
|
||||
}
|
||||
|
||||
// Handle Test Email
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['test_email_addr'])) {
|
||||
if (!canEdit('settings')) {
|
||||
$_SESSION['error'] = 'عذراً، ليس لديك الصلاحية للقيام بهذا الإجراء';
|
||||
} else {
|
||||
$to = $_POST['test_email_addr'];
|
||||
$res = MailService::sendMail($to, "رسالة تجريبية - Test Email", "<p>إذا كنت ترى هذه الرسالة، فإن إعدادات SMTP تعمل بشكل صحيح.</p>");
|
||||
if ($res['success']) {
|
||||
$_SESSION['success'] = "تم إرسال الرسالة التجريبية بنجاح إلى $to";
|
||||
} else {
|
||||
$_SESSION['error'] = "فشل إرسال الرسالة التجريبية: " . $res['error'];
|
||||
}
|
||||
}
|
||||
redirect('charity-settings.php');
|
||||
}
|
||||
|
||||
// Handle Status Operations
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_status'])) {
|
||||
if (!canEdit('settings')) {
|
||||
$_SESSION['error'] = 'عذراً، ليس لديك الصلاحية لتعديل الإعدادات';
|
||||
} else {
|
||||
$name = $_POST['status_name'];
|
||||
$color = $_POST['status_color'];
|
||||
$is_default = isset($_POST['is_default']) ? 1 : 0;
|
||||
if ($is_default) db()->query("UPDATE mailbox_statuses SET is_default = 0");
|
||||
$stmt = db()->prepare("INSERT INTO mailbox_statuses (name, color, is_default) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$name, $color, $is_default]);
|
||||
$_SESSION['success'] = 'تم إضافة نوع الحالة بنجاح';
|
||||
}
|
||||
redirect('charity-settings.php');
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_status'])) {
|
||||
if (!canEdit('settings')) {
|
||||
$_SESSION['error'] = 'عذراً، ليس لديك الصلاحية لتعديل الإعدادات';
|
||||
} else {
|
||||
$id = $_POST['status_id'];
|
||||
$name = $_POST['status_name'];
|
||||
$color = $_POST['status_color'];
|
||||
$is_default = isset($_POST['is_default']) ? 1 : 0;
|
||||
if ($is_default) db()->query("UPDATE mailbox_statuses SET is_default = 0");
|
||||
$stmt = db()->prepare("UPDATE mailbox_statuses SET name = ?, color = ?, is_default = ? WHERE id = ?");
|
||||
$stmt->execute([$name, $color, $is_default, $id]);
|
||||
$_SESSION['success'] = 'تم تحديث نوع الحالة بنجاح';
|
||||
}
|
||||
redirect('charity-settings.php');
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_status'])) {
|
||||
if (!canDelete('settings')) {
|
||||
$_SESSION['error'] = 'عذراً، ليس لديك الصلاحية لحذف الإعدادات';
|
||||
} else {
|
||||
$id = $_POST['status_id'];
|
||||
$count = 0; foreach(['inbound_mail', 'outbound_mail', 'internal_mail'] as $t) { $stmt = db()->prepare("SELECT COUNT(*) FROM $t WHERE status_id = ?"); $stmt->execute([$id]); $count += $stmt->fetchColumn(); }
|
||||
|
||||
if ($count > 0) {
|
||||
$_SESSION['error'] = 'لا يمكن حذف هذه الحالة لأنها مستخدمة في بعض السجلات';
|
||||
} else {
|
||||
db()->prepare("DELETE FROM mailbox_statuses WHERE id = ?")->execute([$id]);
|
||||
$_SESSION['success'] = 'تم حذف نوع الحالة بنجاح';
|
||||
}
|
||||
}
|
||||
redirect('charity-settings.php');
|
||||
}
|
||||
|
||||
// Get session messages
|
||||
if (isset($_SESSION['success'])) {
|
||||
$success_msg = $_SESSION['success'];
|
||||
unset($_SESSION['success']);
|
||||
}
|
||||
if (isset($_SESSION['error'])) {
|
||||
$error_msg = $_SESSION['error'];
|
||||
unset($_SESSION['error']);
|
||||
}
|
||||
|
||||
$statuses = db()->query("SELECT * FROM mailbox_statuses ORDER BY id ASC")->fetchAll();
|
||||
$email_logs = db()->query("SELECT * FROM email_logs ORDER BY id DESC LIMIT 50")->fetchAll();
|
||||
|
||||
// System Info
|
||||
$php_version = phpversion();
|
||||
$mysql_version = db()->query("SELECT VERSION()")->fetchColumn();
|
||||
$server_addr = $_SERVER['SERVER_ADDR'] ?? '127.0.0.1';
|
||||
$upload_max = ini_get('upload_max_filesize');
|
||||
$post_max = ini_get('post_max_size');
|
||||
?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-4">
|
||||
<h2 class="fw-bold"><i class="fas fa-cog me-2"></i> الإعدادات</h2>
|
||||
</div>
|
||||
|
||||
<?php if ($success_msg): ?>
|
||||
<div class="alert alert-success border-0 shadow-sm"><i class="fas fa-check-circle me-2"></i> <?= $success_msg ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($error_msg): ?>
|
||||
<div class="alert alert-danger border-0 shadow-sm"><i class="fas fa-exclamation-circle me-2"></i> <?= $error_msg ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-white p-0">
|
||||
<ul class="nav nav-tabs border-bottom-0" id="settingsTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active px-4 py-3" id="general-tab" data-bs-toggle="tab" data-bs-target="#general" type="button" role="tab">بيانات النظام</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link px-4 py-3" id="smtp-tab" data-bs-toggle="tab" data-bs-target="#smtp" type="button" role="tab">إعدادات SMTP</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link px-4 py-3" id="statuses-tab" data-bs-toggle="tab" data-bs-target="#statuses" type="button" role="tab">حالات البريد</button>
|
||||
</li>
|
||||
<?php if (isSuperAdmin()): ?>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link px-4 py-3 text-danger fw-bold" id="advanced-tab" data-bs-toggle="tab" data-bs-target="#advanced" type="button" role="tab">إعدادات متقدمة</button>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link px-4 py-3" id="logs-tab" data-bs-toggle="tab" data-bs-target="#logs" type="button" role="tab">سجلات البريد</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link px-4 py-3" id="sysinfo-tab" data-bs-toggle="tab" data-bs-target="#sysinfo" type="button" role="tab">معلومات الخادم</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="tab-content" id="settingsTabsContent">
|
||||
<!-- General Settings -->
|
||||
<div class="tab-pane fade show active" id="general" role="tabpanel">
|
||||
<h5 class="fw-bold mb-4 text-primary">بيانات النظام والجهة</h5>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="update_charity" value="1">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">اسم النظام / الجهة</label>
|
||||
<input type="text" name="charity_name" class="form-control" value="<?= htmlspecialchars($charity['charity_name'] ?? '') ?>" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">شعار الجهة (Slogan)</label>
|
||||
<input type="text" name="charity_slogan" class="form-control" value="<?= htmlspecialchars($charity['charity_slogan'] ?? '') ?>" placeholder="أدخل شعار الجهة الذي سيظهر تحت الاسم">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">البريد الإلكتروني الرسمي</label>
|
||||
<input type="email" name="charity_email" class="form-control" value="<?= htmlspecialchars($charity['charity_email'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">رقم الهاتف</label>
|
||||
<input type="text" name="charity_phone" class="form-control" value="<?= htmlspecialchars($charity['charity_phone'] ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">العنوان</label>
|
||||
<textarea name="charity_address" class="form-control" rows="3"><?= htmlspecialchars($charity['charity_address'] ?? '') ?></textarea>
|
||||
</div>
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">شعار النظام</label>
|
||||
<input type="file" name="charity_logo" class="form-control" accept="image/*">
|
||||
<?php if ($charity['charity_logo']): ?>
|
||||
<div class="mt-3 p-3 bg-light rounded text-center border">
|
||||
<img src="<?= $charity['charity_logo'] ?>" alt="Logo" style="max-height: 100px;">
|
||||
<p class="small text-muted mt-2 mb-0">الشعار الحالي</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">أيقونة الموقع (Favicon)</label>
|
||||
<input type="file" name="charity_favicon" class="form-control" accept="image/x-icon,image/png">
|
||||
<?php if ($charity['charity_favicon']): ?>
|
||||
<div class="mt-3 p-3 bg-light rounded text-center border">
|
||||
<img src="<?= $charity['charity_favicon'] ?>" alt="Favicon" style="max-height: 48px;">
|
||||
<p class="small text-muted mt-2 mb-0">الأيقونة الحالية</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end mt-4">
|
||||
<?php if (canEdit('settings')): ?>
|
||||
<button type="submit" class="btn btn-dark px-4">حفظ جميع التغييرات</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- SMTP Settings -->
|
||||
<div class="tab-pane fade" id="smtp" role="tabpanel">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 class="fw-bold text-primary mb-0">إعدادات البريد (SMTP)</h5>
|
||||
<?php if (!$smtp['is_enabled']): ?>
|
||||
<div class="alert alert-danger py-2 px-3 mb-0 d-flex align-items-center">
|
||||
<small><i class="fas fa-exclamation-triangle me-2"></i> SMTP معطل حالياً</small>
|
||||
<?php if (canEdit('settings')): ?>
|
||||
<form method="POST" class="ms-3">
|
||||
<button type="submit" name="enable_smtp" class="btn btn-sm btn-outline-danger">تفعيل الآن</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="badge bg-success p-2">
|
||||
<i class="fas fa-check-circle me-1"></i> SMTP يعمل (الأخطاء: <?= $smtp['consecutive_failures'] ?>/<?= $smtp['max_failures'] ?>)
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="update_smtp" value="1">
|
||||
<div class="row">
|
||||
<div class="col-md-8 mb-3">
|
||||
<label class="form-label fw-bold">SMTP Host</label>
|
||||
<input type="text" name="smtp_host" class="form-control" value="<?= htmlspecialchars($smtp['smtp_host'] ?? '') ?>" placeholder="smtp.example.com">
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-bold">SMTP Port</label>
|
||||
<input type="number" name="smtp_port" class="form-control" value="<?= htmlspecialchars($smtp['smtp_port'] ?? 587) ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">SMTP Security</label>
|
||||
<select name="smtp_secure" class="form-select">
|
||||
<option value="tls" <?= ($smtp['smtp_secure'] ?? '') === 'tls' ? 'selected' : '' ?>>TLS (الأكثر أماناً)</option>
|
||||
<option value="ssl" <?= ($smtp['smtp_secure'] ?? '') === 'ssl' ? 'selected' : '' ?>>SSL</option>
|
||||
<option value="none" <?= ($smtp['smtp_secure'] ?? '') === 'none' ? 'selected' : '' ?>>بدون تشفير</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">اسم المرسل (Display Name)</label>
|
||||
<input type="text" name="from_name" class="form-control" value="<?= htmlspecialchars($smtp['from_name'] ?? '') ?>" placeholder="نظام المراسلات">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">SMTP Username</label>
|
||||
<input type="text" name="smtp_user" class="form-control" value="<?= htmlspecialchars($smtp['smtp_user'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">SMTP Password</label>
|
||||
<input type="password" name="smtp_pass" class="form-control" value="<?= htmlspecialchars($smtp['smtp_pass'] ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-bold">From Email</label>
|
||||
<input type="email" name="from_email" class="form-control" value="<?= htmlspecialchars($smtp['from_email'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-bold">Reply-To Email</label>
|
||||
<input type="email" name="reply_to" class="form-control" value="<?= htmlspecialchars($smtp['reply_to'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-bold">حد الأخطاء قبل التعطيل</label>
|
||||
<input type="number" name="max_failures" class="form-control" value="<?= htmlspecialchars($smtp['max_failures'] ?? 5) ?>">
|
||||
</div>
|
||||
</div>
|
||||
<?php if (canEdit('settings')): ?>
|
||||
<button type="submit" class="btn btn-primary">حفظ إعدادات البريد</button>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<div class="mt-5 p-4 bg-light rounded border">
|
||||
<h6 class="fw-bold mb-3">اختبار الإرسال</h6>
|
||||
<p class="small text-muted mb-3">أدخل بريداً إلكترونياً صالحاً لإرسال رسالة تجريبية للتأكد من صحة الإعدادات.</p>
|
||||
<form method="POST">
|
||||
<div class="input-group" style="max-width: 450px;">
|
||||
<input type="email" name="test_email_addr" class="form-control" placeholder="بريد الوجهة (example@mail.com)" required>
|
||||
<button class="btn btn-secondary" type="submit" <?= !canEdit('settings') ? 'disabled' : '' ?>><i class="fas fa-paper-plane me-2"></i> إرسال اختبار</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Settings (Super Admin Only) -->
|
||||
<?php if (isSuperAdmin()): ?>
|
||||
<div class="tab-pane fade" id="advanced" role="tabpanel">
|
||||
<h5 class="fw-bold mb-4 text-danger">إعدادات النظام المتقدمة</h5>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="update_advanced" value="1">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="p-3 border rounded">
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input class="form-check-input" type="checkbox" name="site_maintenance" id="site_maintenance" <?= $charity['site_maintenance'] ? 'checked' : '' ?>>
|
||||
<label class="form-check-label fw-bold" for="site_maintenance">وضع الصيانة (Maintenance Mode)</label>
|
||||
</div>
|
||||
<p class="small text-muted mb-0">عند تفعيل هذا الوضع، لن يتمكن سوى المديرين من الدخول للنظام.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="p-3 border rounded">
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input class="form-check-input" type="checkbox" name="allow_registration" id="allow_registration" <?= $charity['allow_registration'] ? 'checked' : '' ?>>
|
||||
<label class="form-check-label fw-bold" for="allow_registration">السماح بالتسجيل الذاتي</label>
|
||||
</div>
|
||||
<p class="small text-muted mb-0">تفعيل خيار "إنشاء حساب جديد" في صفحة تسجيل الدخول.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">نص تذييل الموقع (Footer Text)</label>
|
||||
<textarea name="site_footer" class="form-control" rows="3" placeholder="أدخل النص الذي سيظهر في أسفل جميع الصفحات..."><?= htmlspecialchars($charity['site_footer'] ?? '') ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end mt-4">
|
||||
<button type="submit" class="btn btn-danger px-4">حفظ الإعدادات المتقدمة</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Statuses Settings -->
|
||||
<div class="tab-pane fade" id="statuses" role="tabpanel">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 class="fw-bold text-primary mb-0">أنواع حالات البريد</h5>
|
||||
<?php if (canAdd('settings')): ?>
|
||||
<button class="btn btn-sm btn-primary" onclick="new bootstrap.Modal(document.getElementById('addStatusModal')).show()"><i class="fas fa-plus me-2"></i> إضافة حالة جديدة</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle border">
|
||||
<thead class="bg-light">
|
||||
<tr><th>الحالة</th><th>كود اللون</th><th>افتراضية</th><th class="text-end">الإجراء</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($statuses as $status): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="badge px-3 py-2" style="background-color: <?= $status['color'] ?>;">
|
||||
<?= htmlspecialchars($status['name']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><code><?= $status['color'] ?></code></td>
|
||||
<td>
|
||||
<?php if ($status['is_default']): ?>
|
||||
<span class="badge bg-success-subtle text-success border border-success px-2 py-1">نعم</span>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">-</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<?php if (canEdit('settings')): ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="editStatus(<?= $status['id'] ?>, '<?= htmlspecialchars($status['name'], ENT_QUOTES) ?>', '<?= $status['color'] ?>', <?= $status['is_default'] ?>)"><i class="fas fa-edit"></i></button>
|
||||
<?php endif; ?>
|
||||
<?php if (canDelete('settings') && !$status['is_default']): ?>
|
||||
<form method="POST" onsubmit="return confirm('هل أنت متأكد من حذف هذه الحالة؟');" style="display:inline;">
|
||||
<input type="hidden" name="status_id" value="<?= $status['id'] ?>">
|
||||
<input type="hidden" name="delete_status" value="1">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email Logs -->
|
||||
<div class="tab-pane fade" id="logs" role="tabpanel">
|
||||
<h5 class="fw-bold text-primary mb-4">سجلات المراسلات البريدية (آخر 50 عملية)</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover border">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="py-3 ps-3">الوقت والتاريخ</th>
|
||||
<th class="py-3">المستلم</th>
|
||||
<th class="py-3">الموضوع</th>
|
||||
<th class="py-3">الحالة</th>
|
||||
<th class="py-3 pe-3">تفاصيل الخطأ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($email_logs): ?>
|
||||
<?php foreach ($email_logs as $log): ?>
|
||||
<tr>
|
||||
<td class="ps-3 small"><?= date('Y-m-d H:i:s', strtotime($log['created_at'])) ?></td>
|
||||
<td><span class="fw-bold"><?= htmlspecialchars($log['recipient']) ?></span></td>
|
||||
<td class="small"><?= htmlspecialchars($log['subject']) ?></td>
|
||||
<td>
|
||||
<span class="badge bg-<?= $log['status'] === 'success' ? 'success' : 'danger' ?>">
|
||||
<?= $log['status'] === 'success' ? 'تم الإرسال' : 'فشل' ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="pe-3 small text-danger"><?= htmlspecialchars($log['error_message'] ?? '-') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="5" class="text-center py-4 text-muted">لا يوجد سجلات حالياً</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Info Tab -->
|
||||
<div class="tab-pane fade" id="sysinfo" role="tabpanel">
|
||||
<h5 class="fw-bold text-primary mb-4">معلومات النظام والخادم</h5>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="p-3 border rounded bg-light">
|
||||
<h6 class="fw-bold border-bottom pb-2 mb-3">بيئة البرمجيات</h6>
|
||||
<table class="table table-sm table-borderless mb-0">
|
||||
<tr><td width="150" class="text-muted">نسخة PHP:</td><td class="fw-bold"><?= $php_version ?></td></tr>
|
||||
<tr><td class="text-muted">نسخة MySQL:</td><td class="fw-bold"><?= $mysql_version ?></td></tr>
|
||||
<tr><td class="text-muted">نظام التشغيل:</td><td class="fw-bold"><?= PHP_OS ?></td></tr>
|
||||
<tr><td class="text-muted">عنوان الخادم:</td><td class="fw-bold"><?= $server_addr ?></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="p-3 border rounded bg-light">
|
||||
<h6 class="fw-bold border-bottom pb-2 mb-3">إعدادات الملفات</h6>
|
||||
<table class="table table-sm table-borderless mb-0">
|
||||
<tr><td width="150" class="text-muted">أقصى حجم رفع:</td><td class="fw-bold"><?= $upload_max ?></td></tr>
|
||||
<tr><td class="text-muted">أقصى حجم POST:</td><td class="fw-bold"><?= $post_max ?></td></tr>
|
||||
<tr><td class="text-muted">ترميز قاعدة البيانات:</td><td class="fw-bold">utf8mb4_unicode_ci</td></tr>
|
||||
<tr><td class="text-muted">نطاق النظام:</td><td class="fw-bold"><?= $_SERVER['HTTP_HOST'] ?></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<div class="p-3 border rounded bg-warning bg-opacity-10 border-warning">
|
||||
<h6 class="fw-bold mb-2 text-warning-emphasis"><i class="fas fa-tools me-2"></i> أدوات الصيانة</h6>
|
||||
<p class="small mb-3">هذه الأدوات مخصصة لمدير النظام فقط. يرجى توخي الحذر عند الاستخدام.</p>
|
||||
<div class="d-flex gap-2">
|
||||
<?php if (canEdit('settings')): ?>
|
||||
<button class="btn btn-sm btn-outline-warning" onclick="alert('قريباً: نسخة احتياطية لقاعدة البيانات')"><i class="fas fa-database me-1"></i> نسخة احتياطية</button>
|
||||
<?php endif; ?>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="location.reload()"><i class="fas fa-sync-alt me-1"></i> تحديث الحالة</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Status Modal -->
|
||||
<div class="modal fade" id="addStatusModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title fw-bold">إضافة حالة جديدة</h5>
|
||||
<button type="button" class="btn-close ms-0 me-auto" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="add_status" value="1">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">اسم الحالة</label>
|
||||
<input type="text" name="status_name" class="form-control" placeholder="مثال: تحت الدراسة" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">اللون التعريفي</label>
|
||||
<input type="color" name="status_color" class="form-control form-control-color w-100" value="#0d6efd">
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="is_default" id="add_is_default">
|
||||
<label class="form-check-label" for="add_is_default">تعيين كحالة افتراضية للبريد الجديد</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary px-4">إضافة الحالة</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Status Modal -->
|
||||
<div class="modal fade" id="editStatusModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title fw-bold">تعديل نوع الحالة</h5>
|
||||
<button type="button" class="btn-close ms-0 me-auto" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="update_status" value="1">
|
||||
<input type="hidden" name="status_id" id="edit_status_id">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">اسم الحالة</label>
|
||||
<input type="text" name="status_name" id="edit_status_name" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">اللون التعريفي</label>
|
||||
<input type="color" name="status_color" id="edit_status_color" class="form-control form-control-color w-100">
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="is_default" id="edit_is_default">
|
||||
<label class="form-check-label" for="edit_is_default">تعيين كحالة افتراضية</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary px-4">حفظ التغييرات</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function editStatus(id, name, color, isDefault) {
|
||||
document.getElementById('edit_status_id').value = id;
|
||||
document.getElementById('edit_status_name').value = name;
|
||||
document.getElementById('edit_status_color').value = color;
|
||||
document.getElementById('edit_is_default').checked = isDefault == 1;
|
||||
new bootstrap.Modal(document.getElementById('editStatusModal')).show();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Preserve active tab after redirect
|
||||
var activeTab = localStorage.getItem('activeSettingsTab');
|
||||
if (activeTab) {
|
||||
var tabEl = document.querySelector('button[data-bs-target="' + activeTab + '"]');
|
||||
if (tabEl) {
|
||||
// Check if it's already shown to avoid flicker
|
||||
if (!tabEl.classList.contains('active')) {
|
||||
bootstrap.Tab.getInstance(tabEl)?.show() || new bootstrap.Tab(tabEl).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(function(tab) {
|
||||
tab.addEventListener('shown.bs.tab', function(e) {
|
||||
localStorage.setItem('activeSettingsTab', e.target.getAttribute('data-bs-target'));
|
||||
});
|
||||
});
|
||||
|
||||
// Handle hash in URL for direct tab access
|
||||
if (window.location.hash) {
|
||||
var hashTab = document.querySelector('button[data-bs-target="' + window.location.hash + '"]');
|
||||
if (hashTab) {
|
||||
bootstrap.Tab.getInstance(hashTab)?.show() || new bootstrap.Tab(hashTab).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@ -15,3 +15,37 @@ function db() {
|
||||
}
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the next reference number for a given type (inbound/outbound/internal)
|
||||
* Format: IN-Year-Serial or OUT-Year-Serial or INT-Year-Serial
|
||||
*/
|
||||
function generateRefNo($type) {
|
||||
$prefix = 'IN';
|
||||
$table = 'inbound_mail';
|
||||
if ($type === 'outbound') {
|
||||
$prefix = 'OUT';
|
||||
$table = 'outbound_mail';
|
||||
} elseif ($type === 'internal') {
|
||||
$prefix = 'INT';
|
||||
$table = 'internal_mail';
|
||||
}
|
||||
|
||||
$year = date('Y');
|
||||
$pattern = $prefix . '-' . $year . '-%';
|
||||
|
||||
// Query the specific table for the type
|
||||
$stmt = db()->prepare("SELECT ref_no FROM $table WHERE ref_no LIKE ? ORDER BY id DESC LIMIT 1");
|
||||
$stmt->execute([$pattern]);
|
||||
$last_ref = $stmt->fetchColumn();
|
||||
|
||||
$serial = 1;
|
||||
if ($last_ref) {
|
||||
$parts = explode('-', $last_ref);
|
||||
if (count($parts) === 3) {
|
||||
$serial = (int)$parts[2] + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $prefix . '-' . $year . '-' . str_pad($serial, 3, '0', STR_PAD_LEFT);
|
||||
}
|
||||
4
db/migrate.php
Normal file
4
db/migrate.php
Normal file
@ -0,0 +1,4 @@
|
||||
<?php
|
||||
// db/migrate.php
|
||||
// Wrapper to run migrations from db/migrations/migrate.php
|
||||
require_once __DIR__ . '/migrations/migrate.php';
|
||||
52
db/migrations/001_initial_schema.sql
Normal file
52
db/migrations/001_initial_schema.sql
Normal file
@ -0,0 +1,52 @@
|
||||
-- Migration: Initial Schema
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
full_name VARCHAR(100),
|
||||
role ENUM('admin', 'clerk', 'staff') DEFAULT 'staff',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mailbox (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
type ENUM('inbound', 'outbound') NOT NULL,
|
||||
ref_no VARCHAR(50) NOT NULL UNIQUE,
|
||||
date_registered DATE NOT NULL,
|
||||
sender VARCHAR(255),
|
||||
recipient VARCHAR(255),
|
||||
subject VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
status ENUM('received', 'in_progress', 'closed') DEFAULT 'received',
|
||||
assigned_to INT,
|
||||
created_by INT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (assigned_to) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS attachments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
mail_id INT NOT NULL,
|
||||
file_path VARCHAR(255) NOT NULL,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
file_size INT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (mail_id) REFERENCES mailbox(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS comments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
mail_id INT NOT NULL,
|
||||
user_id INT,
|
||||
comment TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (mail_id) REFERENCES mailbox(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Initial Admin User (password: admin123)
|
||||
INSERT INTO users (username, password, full_name, role)
|
||||
VALUES ('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'مدير النظام', 'admin')
|
||||
ON DUPLICATE KEY UPDATE username=username;
|
||||
15
db/migrations/002_add_profiles.sql
Normal file
15
db/migrations/002_add_profiles.sql
Normal file
@ -0,0 +1,15 @@
|
||||
-- Migration: Add User and Charity Profiles
|
||||
ALTER TABLE users ADD COLUMN profile_image VARCHAR(255) DEFAULT NULL AFTER full_name;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS charity_settings (
|
||||
id INT PRIMARY KEY DEFAULT 1,
|
||||
charity_name VARCHAR(255) NOT NULL DEFAULT 'جمعية خيرية',
|
||||
charity_logo VARCHAR(255) DEFAULT NULL,
|
||||
charity_favicon VARCHAR(255) DEFAULT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT single_row CHECK (id = 1)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Initial Charity Settings
|
||||
INSERT INTO charity_settings (id, charity_name) VALUES (1, 'بريد الجمعية الخيرية')
|
||||
ON DUPLICATE KEY UPDATE charity_name=charity_name;
|
||||
10
db/migrations/003_add_fields_to_profiles.sql
Normal file
10
db/migrations/003_add_fields_to_profiles.sql
Normal file
@ -0,0 +1,10 @@
|
||||
-- Migration: Add extra fields to User and Charity Profiles
|
||||
ALTER TABLE users
|
||||
ADD COLUMN email VARCHAR(255) DEFAULT NULL AFTER full_name,
|
||||
ADD COLUMN phone VARCHAR(50) DEFAULT NULL AFTER email,
|
||||
ADD COLUMN address TEXT DEFAULT NULL AFTER phone;
|
||||
|
||||
ALTER TABLE charity_settings
|
||||
ADD COLUMN charity_email VARCHAR(255) DEFAULT NULL AFTER charity_name,
|
||||
ADD COLUMN charity_phone VARCHAR(50) DEFAULT NULL AFTER charity_email,
|
||||
ADD COLUMN charity_address TEXT DEFAULT NULL AFTER charity_phone;
|
||||
25
db/migrations/004_add_mailbox_statuses.sql
Normal file
25
db/migrations/004_add_mailbox_statuses.sql
Normal file
@ -0,0 +1,25 @@
|
||||
-- Migration: Add mailbox statuses table
|
||||
CREATE TABLE IF NOT EXISTS mailbox_statuses (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
color VARCHAR(20) DEFAULT '#000000',
|
||||
is_default BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Insert initial statuses if they don't exist
|
||||
INSERT IGNORE INTO mailbox_statuses (id, name, color, is_default) VALUES
|
||||
(1, 'received', '#6c757d', TRUE),
|
||||
(2, 'in_progress', '#0d6efd', FALSE),
|
||||
(3, 'closed', '#198754', FALSE);
|
||||
|
||||
-- Add status_id to mailbox
|
||||
ALTER TABLE mailbox ADD COLUMN IF NOT EXISTS status_id INT;
|
||||
|
||||
-- Update status_id based on existing ENUM values
|
||||
UPDATE mailbox SET status_id = 1 WHERE status = 'received' AND status_id IS NULL;
|
||||
UPDATE mailbox SET status_id = 2 WHERE status = 'in_progress' AND status_id IS NULL;
|
||||
UPDATE mailbox SET status_id = 3 WHERE status = 'closed' AND status_id IS NULL;
|
||||
|
||||
-- Set default status_id for any NULLs
|
||||
UPDATE mailbox SET status_id = 1 WHERE status_id IS NULL;
|
||||
2
db/migrations/005_add_due_date_to_mailbox.sql
Normal file
2
db/migrations/005_add_due_date_to_mailbox.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- Migration: Add due_date column to mailbox table
|
||||
ALTER TABLE mailbox ADD COLUMN IF NOT EXISTS due_date DATE NULL;
|
||||
18
db/migrations/006_add_smtp_settings.sql
Normal file
18
db/migrations/006_add_smtp_settings.sql
Normal file
@ -0,0 +1,18 @@
|
||||
CREATE TABLE IF NOT EXISTS smtp_settings (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
transport VARCHAR(20) DEFAULT 'smtp',
|
||||
smtp_host VARCHAR(255),
|
||||
smtp_port INT DEFAULT 587,
|
||||
smtp_secure VARCHAR(10) DEFAULT 'tls',
|
||||
smtp_user VARCHAR(255),
|
||||
smtp_pass VARCHAR(255),
|
||||
from_email VARCHAR(255),
|
||||
from_name VARCHAR(255),
|
||||
reply_to VARCHAR(255),
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Insert default row if not exists
|
||||
INSERT INTO smtp_settings (id, transport, smtp_host, smtp_port, smtp_secure, from_email, from_name)
|
||||
SELECT 1, 'smtp', '', 587, 'tls', 'no-reply@localhost', 'App'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM smtp_settings WHERE id = 1);
|
||||
14
db/migrations/007_add_email_logs.sql
Normal file
14
db/migrations/007_add_email_logs.sql
Normal file
@ -0,0 +1,14 @@
|
||||
-- Create email_logs table
|
||||
CREATE TABLE IF NOT EXISTS email_logs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
recipient TEXT NOT NULL,
|
||||
subject VARCHAR(255),
|
||||
status ENUM('success', 'failure') NOT NULL,
|
||||
error_message TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Add consecutive_failures to smtp_settings to track repeated issues
|
||||
ALTER TABLE smtp_settings ADD COLUMN consecutive_failures INT DEFAULT 0;
|
||||
ALTER TABLE smtp_settings ADD COLUMN max_failures INT DEFAULT 5;
|
||||
ALTER TABLE smtp_settings ADD COLUMN is_enabled TINYINT(1) DEFAULT 1;
|
||||
3
db/migrations/008_add_password_reset.sql
Normal file
3
db/migrations/008_add_password_reset.sql
Normal file
@ -0,0 +1,3 @@
|
||||
-- Add password reset token columns to users table
|
||||
ALTER TABLE users ADD COLUMN reset_token VARCHAR(100) NULL;
|
||||
ALTER TABLE users ADD COLUMN reset_token_expiry DATETIME NULL;
|
||||
2
db/migrations/009_add_theme_to_users.sql
Normal file
2
db/migrations/009_add_theme_to_users.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- Migration: Add theme to users
|
||||
ALTER TABLE users ADD COLUMN theme VARCHAR(20) DEFAULT 'light';
|
||||
11
db/migrations/010_add_granular_permissions.sql
Normal file
11
db/migrations/010_add_granular_permissions.sql
Normal file
@ -0,0 +1,11 @@
|
||||
-- Migration: Add granular permissions to users table
|
||||
ALTER TABLE users
|
||||
ADD COLUMN can_view TINYINT(1) DEFAULT 1,
|
||||
ADD COLUMN can_add TINYINT(1) DEFAULT 0,
|
||||
ADD COLUMN can_edit TINYINT(1) DEFAULT 0,
|
||||
ADD COLUMN can_delete TINYINT(1) DEFAULT 0;
|
||||
|
||||
-- Set defaults for existing roles
|
||||
UPDATE users SET can_view = 1, can_add = 1, can_edit = 1, can_delete = 1 WHERE role = 'admin';
|
||||
UPDATE users SET can_view = 1, can_add = 1, can_edit = 1, can_delete = 0 WHERE role = 'clerk';
|
||||
UPDATE users SET can_view = 1, can_add = 0, can_edit = 0, can_delete = 0 WHERE role = 'staff';
|
||||
3
db/migrations/011_add_referral_to_comments.sql
Normal file
3
db/migrations/011_add_referral_to_comments.sql
Normal file
@ -0,0 +1,3 @@
|
||||
-- Migration: Add referred_user_id to comments
|
||||
ALTER TABLE comments ADD COLUMN referred_user_id INT DEFAULT NULL;
|
||||
ALTER TABLE comments ADD CONSTRAINT fk_comments_referred_user FOREIGN KEY (referred_user_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||
2
db/migrations/012_add_internal_mail_type.sql
Normal file
2
db/migrations/012_add_internal_mail_type.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- Migration: Add internal mail type to mailbox table
|
||||
ALTER TABLE mailbox MODIFY COLUMN type ENUM('inbound', 'outbound', 'internal') NOT NULL;
|
||||
50
db/migrations/013_add_page_permissions.sql
Normal file
50
db/migrations/013_add_page_permissions.sql
Normal file
@ -0,0 +1,50 @@
|
||||
-- Migration: Add per-page granular permissions
|
||||
CREATE TABLE IF NOT EXISTS user_permissions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
page VARCHAR(50) NOT NULL,
|
||||
can_view TINYINT(1) DEFAULT 0,
|
||||
can_add TINYINT(1) DEFAULT 0,
|
||||
can_edit TINYINT(1) DEFAULT 0,
|
||||
can_delete TINYINT(1) DEFAULT 0,
|
||||
UNIQUE KEY user_page (user_id, page),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Seed permissions for existing users based on their roles
|
||||
-- Inbound Mail
|
||||
INSERT IGNORE INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete)
|
||||
SELECT id, 'inbound', can_view, can_add, can_edit, can_delete FROM users;
|
||||
|
||||
-- Outbound Mail
|
||||
INSERT IGNORE INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete)
|
||||
SELECT id, 'outbound', can_view, can_add, can_edit, can_delete FROM users;
|
||||
|
||||
-- Internal Mail
|
||||
INSERT IGNORE INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete)
|
||||
SELECT id, 'internal', can_view, can_add, can_edit, can_delete FROM users;
|
||||
|
||||
-- Users (Only Admins)
|
||||
INSERT IGNORE INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete)
|
||||
SELECT id, 'users',
|
||||
IF(role = 'admin', 1, 0),
|
||||
IF(role = 'admin', 1, 0),
|
||||
IF(role = 'admin', 1, 0),
|
||||
IF(role = 'admin', 1, 0)
|
||||
FROM users;
|
||||
|
||||
-- Settings (Only Admins)
|
||||
INSERT IGNORE INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete)
|
||||
SELECT id, 'settings',
|
||||
IF(role = 'admin', 1, 0),
|
||||
IF(role = 'admin', 1, 0),
|
||||
IF(role = 'admin', 1, 0),
|
||||
IF(role = 'admin', 1, 0)
|
||||
FROM users;
|
||||
|
||||
-- Reports
|
||||
INSERT IGNORE INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete)
|
||||
SELECT id, 'reports',
|
||||
IF(role IN ('admin', 'clerk'), 1, 0),
|
||||
0, 0, 0
|
||||
FROM users;
|
||||
4
db/migrations/014_add_display_name_to_attachments.sql
Normal file
4
db/migrations/014_add_display_name_to_attachments.sql
Normal file
@ -0,0 +1,4 @@
|
||||
-- Migration: Add display_name to attachments table
|
||||
-- This column is needed for the split_mailbox migration (015) to work correctly
|
||||
|
||||
ALTER TABLE attachments ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) AFTER mail_id;
|
||||
11
db/migrations/015_add_super_admin_and_extra_settings.sql
Normal file
11
db/migrations/015_add_super_admin_and_extra_settings.sql
Normal file
@ -0,0 +1,11 @@
|
||||
-- Migration: Add Super Admin and Extra Settings
|
||||
ALTER TABLE users ADD COLUMN is_super_admin TINYINT(1) DEFAULT 0 AFTER role;
|
||||
|
||||
-- Mark initial admin as super admin
|
||||
UPDATE users SET is_super_admin = 1 WHERE username = 'admin';
|
||||
|
||||
-- Add more settings to charity_settings
|
||||
ALTER TABLE charity_settings
|
||||
ADD COLUMN site_maintenance TINYINT(1) DEFAULT 0,
|
||||
ADD COLUMN site_footer TEXT DEFAULT NULL,
|
||||
ADD COLUMN allow_registration TINYINT(1) DEFAULT 0;
|
||||
209
db/migrations/015_split_mailbox_tables.sql
Normal file
209
db/migrations/015_split_mailbox_tables.sql
Normal file
@ -0,0 +1,209 @@
|
||||
-- Migration: Split mailbox into separate tables for each module
|
||||
-- This addresses the architectural concern of having all modules in a single table
|
||||
|
||||
-- 1. Create INBOUND tables
|
||||
CREATE TABLE IF NOT EXISTS inbound_mail (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
ref_no VARCHAR(50) NOT NULL UNIQUE,
|
||||
date_registered DATE NOT NULL,
|
||||
due_date DATE NULL,
|
||||
sender VARCHAR(255),
|
||||
recipient VARCHAR(255),
|
||||
subject VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
status_id INT,
|
||||
assigned_to INT,
|
||||
created_by INT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (assigned_to) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS inbound_attachments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
mail_id INT NOT NULL,
|
||||
display_name VARCHAR(255),
|
||||
file_path VARCHAR(255) NOT NULL,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
file_size INT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (mail_id) REFERENCES inbound_mail(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS inbound_comments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
mail_id INT NOT NULL,
|
||||
user_id INT,
|
||||
comment TEXT NOT NULL,
|
||||
referred_user_id INT DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (mail_id) REFERENCES inbound_mail(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (referred_user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 2. Create OUTBOUND tables
|
||||
CREATE TABLE IF NOT EXISTS outbound_mail (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
ref_no VARCHAR(50) NOT NULL UNIQUE,
|
||||
date_registered DATE NOT NULL,
|
||||
due_date DATE NULL,
|
||||
sender VARCHAR(255),
|
||||
recipient VARCHAR(255),
|
||||
subject VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
status_id INT,
|
||||
assigned_to INT,
|
||||
created_by INT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (assigned_to) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS outbound_attachments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
mail_id INT NOT NULL,
|
||||
display_name VARCHAR(255),
|
||||
file_path VARCHAR(255) NOT NULL,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
file_size INT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (mail_id) REFERENCES outbound_mail(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS outbound_comments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
mail_id INT NOT NULL,
|
||||
user_id INT,
|
||||
comment TEXT NOT NULL,
|
||||
referred_user_id INT DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (mail_id) REFERENCES outbound_mail(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (referred_user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 3. Create INTERNAL tables
|
||||
CREATE TABLE IF NOT EXISTS internal_mail (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
ref_no VARCHAR(50) NOT NULL UNIQUE,
|
||||
date_registered DATE NOT NULL,
|
||||
due_date DATE NULL,
|
||||
sender VARCHAR(255),
|
||||
recipient VARCHAR(255),
|
||||
subject VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
status_id INT,
|
||||
assigned_to INT,
|
||||
created_by INT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (assigned_to) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS internal_attachments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
mail_id INT NOT NULL,
|
||||
display_name VARCHAR(255),
|
||||
file_path VARCHAR(255) NOT NULL,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
file_size INT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (mail_id) REFERENCES internal_mail(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS internal_comments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
mail_id INT NOT NULL,
|
||||
user_id INT,
|
||||
comment TEXT NOT NULL,
|
||||
referred_user_id INT DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (mail_id) REFERENCES internal_mail(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (referred_user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 4. Migrate data (using INSERT IGNORE to allow re-running partially failed migrations)
|
||||
-- We wrap these in conditional checks to ensure they only run if 'mailbox' exists.
|
||||
|
||||
SET @mailbox_exists = (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'mailbox');
|
||||
|
||||
-- Inbound Mail
|
||||
SET @sql = IF(@mailbox_exists > 0,
|
||||
"INSERT IGNORE INTO inbound_mail (id, ref_no, date_registered, due_date, sender, recipient, subject, description, status_id, assigned_to, created_by, created_at, updated_at) SELECT id, ref_no, date_registered, due_date, sender, recipient, subject, description, status_id, assigned_to, created_by, created_at, updated_at FROM mailbox WHERE type = 'inbound'",
|
||||
"SELECT 1");
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(@mailbox_exists > 0,
|
||||
"INSERT IGNORE INTO inbound_attachments (id, mail_id, display_name, file_path, file_name, file_size, created_at) SELECT a.id, a.mail_id, a.display_name, a.file_path, a.file_name, a.file_size, a.created_at FROM attachments a JOIN mailbox m ON a.mail_id = m.id WHERE m.type = 'inbound'",
|
||||
"SELECT 1");
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(@mailbox_exists > 0,
|
||||
"INSERT IGNORE INTO inbound_comments (id, mail_id, user_id, comment, referred_user_id, created_at) SELECT c.id, c.mail_id, c.user_id, c.comment, c.referred_user_id, c.created_at FROM comments c JOIN mailbox m ON c.mail_id = m.id WHERE m.type = 'inbound'",
|
||||
"SELECT 1");
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Outbound Mail
|
||||
SET @sql = IF(@mailbox_exists > 0,
|
||||
"INSERT IGNORE INTO outbound_mail (id, ref_no, date_registered, due_date, sender, recipient, subject, description, status_id, assigned_to, created_by, created_at, updated_at) SELECT id, ref_no, date_registered, due_date, sender, recipient, subject, description, status_id, assigned_to, created_by, created_at, updated_at FROM mailbox WHERE type = 'outbound'",
|
||||
"SELECT 1");
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(@mailbox_exists > 0,
|
||||
"INSERT IGNORE INTO outbound_attachments (id, mail_id, display_name, file_path, file_name, file_size, created_at) SELECT a.id, a.mail_id, a.display_name, a.file_path, a.file_name, a.file_size, a.created_at FROM attachments a JOIN mailbox m ON a.mail_id = m.id WHERE m.type = 'outbound'",
|
||||
"SELECT 1");
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(@mailbox_exists > 0,
|
||||
"INSERT IGNORE INTO outbound_comments (id, mail_id, user_id, comment, referred_user_id, created_at) SELECT c.id, c.mail_id, c.user_id, c.comment, c.referred_user_id, c.created_at FROM comments c JOIN mailbox m ON c.mail_id = m.id WHERE m.type = 'outbound'",
|
||||
"SELECT 1");
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Internal Mail
|
||||
SET @sql = IF(@mailbox_exists > 0,
|
||||
"INSERT IGNORE INTO internal_mail (id, ref_no, date_registered, due_date, sender, recipient, subject, description, status_id, assigned_to, created_by, created_at, updated_at) SELECT id, ref_no, date_registered, due_date, sender, recipient, subject, description, status_id, assigned_to, created_by, created_at, updated_at FROM mailbox WHERE type = 'internal'",
|
||||
"SELECT 1");
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(@mailbox_exists > 0,
|
||||
"INSERT IGNORE INTO internal_attachments (id, mail_id, display_name, file_path, file_name, file_size, created_at) SELECT a.id, a.mail_id, a.display_name, a.file_path, a.file_name, a.file_size, a.created_at FROM attachments a JOIN mailbox m ON a.mail_id = m.id WHERE m.type = 'internal'",
|
||||
"SELECT 1");
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(@mailbox_exists > 0,
|
||||
"INSERT IGNORE INTO internal_comments (id, mail_id, user_id, comment, referred_user_id, created_at) SELECT c.id, c.mail_id, c.user_id, c.comment, c.referred_user_id, c.created_at FROM comments c JOIN mailbox m ON c.mail_id = m.id WHERE m.type = 'internal'",
|
||||
"SELECT 1");
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 5. Rename old tables instead of dropping for safety
|
||||
-- Only rename if 'mailbox' exists and 'mailbox_old' does not
|
||||
SET @old_mailbox = (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'mailbox_old');
|
||||
SET @mailbox_exists = (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'mailbox');
|
||||
|
||||
SET @sql_rename = IF(@old_mailbox = 0 AND @mailbox_exists > 0, 'RENAME TABLE mailbox TO mailbox_old, attachments TO attachments_old, comments TO comments_old', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql_rename;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
2
db/migrations/017_add_slogan_to_settings.sql
Normal file
2
db/migrations/017_add_slogan_to_settings.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- Add slogan field to charity_settings table
|
||||
ALTER TABLE charity_settings ADD COLUMN IF NOT EXISTS charity_slogan VARCHAR(255) DEFAULT NULL AFTER charity_name;
|
||||
26
db/migrations/018_add_accounting_module.sql
Normal file
26
db/migrations/018_add_accounting_module.sql
Normal file
@ -0,0 +1,26 @@
|
||||
-- Accounting module tables
|
||||
CREATE TABLE IF NOT EXISTS accounting_journal (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
date DATE NOT NULL,
|
||||
description TEXT,
|
||||
reference VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS accounting_entries (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
journal_id INT NOT NULL,
|
||||
account_name VARCHAR(255) NOT NULL,
|
||||
debit DECIMAL(15,2) DEFAULT 0.00,
|
||||
credit DECIMAL(15,2) DEFAULT 0.00,
|
||||
FOREIGN KEY (journal_id) REFERENCES accounting_journal(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Register accounting module in user permissions
|
||||
INSERT IGNORE INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete)
|
||||
SELECT id, 'accounting',
|
||||
IF(role = 'admin', 1, 0),
|
||||
IF(role = 'admin', 1, 0),
|
||||
IF(role = 'admin', 1, 0),
|
||||
IF(role = 'admin', 1, 0)
|
||||
FROM users;
|
||||
5
db/migrations/019_add_accounts_table.sql
Normal file
5
db/migrations/019_add_accounts_table.sql
Normal file
@ -0,0 +1,5 @@
|
||||
CREATE TABLE IF NOT EXISTS accounting_accounts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type ENUM('Assets', 'Liabilities', 'Equity', 'Revenue', 'Expenses') NOT NULL
|
||||
);
|
||||
134
db/migrations/020_add_hr_module.sql
Normal file
134
db/migrations/020_add_hr_module.sql
Normal file
@ -0,0 +1,134 @@
|
||||
-- Migration: Add HR Module
|
||||
-- 1. Departments
|
||||
CREATE TABLE IF NOT EXISTS hr_departments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 2. Employees (Linked to users optionally)
|
||||
CREATE TABLE IF NOT EXISTS hr_employees (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT UNIQUE DEFAULT NULL, -- Link to system login if applicable
|
||||
department_id INT,
|
||||
first_name VARCHAR(50) NOT NULL,
|
||||
last_name VARCHAR(50) NOT NULL,
|
||||
email VARCHAR(100),
|
||||
phone VARCHAR(20),
|
||||
gender ENUM('male', 'female') DEFAULT 'male',
|
||||
birth_date DATE,
|
||||
join_date DATE NOT NULL,
|
||||
job_title VARCHAR(100),
|
||||
basic_salary DECIMAL(10, 2) DEFAULT 0.00,
|
||||
status ENUM('active', 'terminated', 'resigned', 'on_leave') DEFAULT 'active',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (department_id) REFERENCES hr_departments(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 3. Attendance
|
||||
CREATE TABLE IF NOT EXISTS hr_attendance (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
employee_id INT NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
check_in TIME,
|
||||
check_out TIME,
|
||||
status ENUM('present', 'absent', 'late', 'excused', 'holiday') DEFAULT 'absent',
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY emp_date (employee_id, date),
|
||||
FOREIGN KEY (employee_id) REFERENCES hr_employees(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 4. Leaves
|
||||
CREATE TABLE IF NOT EXISTS hr_leaves (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
employee_id INT NOT NULL,
|
||||
leave_type ENUM('annual', 'sick', 'unpaid', 'maternity', 'emergency', 'other') NOT NULL,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
days_count INT DEFAULT 1,
|
||||
reason TEXT,
|
||||
status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending',
|
||||
approved_by INT DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (employee_id) REFERENCES hr_employees(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (approved_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 5. Holidays
|
||||
CREATE TABLE IF NOT EXISTS hr_holidays (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
date_from DATE NOT NULL,
|
||||
date_to DATE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 6. Payroll
|
||||
CREATE TABLE IF NOT EXISTS hr_payroll (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
employee_id INT NOT NULL,
|
||||
month INT NOT NULL,
|
||||
year INT NOT NULL,
|
||||
basic_salary DECIMAL(10, 2) NOT NULL,
|
||||
bonuses DECIMAL(10, 2) DEFAULT 0.00,
|
||||
deductions DECIMAL(10, 2) DEFAULT 0.00,
|
||||
net_salary DECIMAL(10, 2) NOT NULL,
|
||||
status ENUM('pending', 'paid') DEFAULT 'pending',
|
||||
payment_date DATE,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY emp_period (employee_id, month, year),
|
||||
FOREIGN KEY (employee_id) REFERENCES hr_employees(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Permissions for HR module (Assign to Admin by default)
|
||||
INSERT IGNORE INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete)
|
||||
SELECT id, 'hr_dashboard',
|
||||
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, 'hr_employees',
|
||||
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, 'hr_attendance',
|
||||
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, 'hr_leaves',
|
||||
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, 'hr_payroll',
|
||||
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, 'hr_reports',
|
||||
IF(role = 'admin', 1, 0),
|
||||
IF(role = 'admin', 1, 0),
|
||||
IF(role = 'admin', 1, 0),
|
||||
IF(role = 'admin', 1, 0)
|
||||
FROM users;
|
||||
76
db/migrations/021_add_stock_module.sql
Normal file
76
db/migrations/021_add_stock_module.sql
Normal file
@ -0,0 +1,76 @@
|
||||
-- 021_add_stock_module.sql
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stock_stores (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
location VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stock_categories (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stock_items (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
category_id INT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
sku VARCHAR(100),
|
||||
description TEXT,
|
||||
min_quantity INT DEFAULT 0,
|
||||
unit VARCHAR(50) DEFAULT 'piece',
|
||||
image_path VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (category_id) REFERENCES stock_categories(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stock_quantities (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
store_id INT NOT NULL,
|
||||
item_id INT NOT NULL,
|
||||
quantity DECIMAL(10,2) DEFAULT 0,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_item_store (store_id, item_id),
|
||||
FOREIGN KEY (store_id) REFERENCES stock_stores(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (item_id) REFERENCES stock_items(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stock_transactions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
transaction_type ENUM('in', 'out', 'transfer', 'damage', 'lend', 'return') NOT NULL,
|
||||
store_id INT NOT NULL,
|
||||
item_id INT NOT NULL,
|
||||
quantity DECIMAL(10,2) NOT NULL,
|
||||
user_id INT,
|
||||
reference VARCHAR(255),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (store_id) REFERENCES stock_stores(id),
|
||||
FOREIGN KEY (item_id) REFERENCES stock_items(id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stock_lending (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
transaction_id INT NOT NULL,
|
||||
borrower_name VARCHAR(255) NOT NULL,
|
||||
borrower_phone VARCHAR(50),
|
||||
expected_return_date DATE,
|
||||
return_transaction_id INT,
|
||||
status ENUM('active', 'returned', 'overdue') DEFAULT 'active',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (transaction_id) REFERENCES stock_transactions(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Add default store if none exists
|
||||
INSERT INTO stock_stores (name, location)
|
||||
SELECT 'المستودع الرئيسي', 'المقر الرئيسي'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM stock_stores);
|
||||
|
||||
-- Add default category
|
||||
INSERT INTO stock_categories (name)
|
||||
SELECT 'عام'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM stock_categories);
|
||||
53
db/migrations/022_add_expenses_module.sql
Normal file
53
db/migrations/022_add_expenses_module.sql
Normal 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;
|
||||
12
db/migrations/023_translate_expense_categories.sql
Normal file
12
db/migrations/023_translate_expense_categories.sql
Normal file
@ -0,0 +1,12 @@
|
||||
-- Translate default expense categories to Arabic
|
||||
|
||||
UPDATE expense_categories SET name = 'مستلزمات مكتبية', description = 'قرطاسية، أحبار، ورق، إلخ' WHERE name = 'Office Supplies';
|
||||
UPDATE expense_categories SET name = 'سفر وتنقلات', description = 'تذاكر طيران، فنادق، مواصلات' WHERE name = 'Travel';
|
||||
UPDATE expense_categories SET name = 'وجبات وضيافة', description = 'غداء عمل، فعاليات الفريق' WHERE name = 'Meals & Entertainment';
|
||||
UPDATE expense_categories SET name = 'مرافق وخدمات', description = 'كهرباء، مياه، انترنت' WHERE name = 'Utilities';
|
||||
UPDATE expense_categories SET name = 'إيجار', description = 'إيجار المكتب' WHERE name = 'Rent';
|
||||
UPDATE expense_categories SET name = 'رواتب وأجور', description = 'رواتب الموظفين' WHERE name = 'Salaries';
|
||||
UPDATE expense_categories SET name = 'صيانة وإصلاحات', description = 'صيانة دورية وإصلاح أعطال' WHERE name = 'Maintenance';
|
||||
UPDATE expense_categories SET name = 'تسويق وإعلان', description = 'حملات إعلانية وترويج' WHERE name = 'Advertising';
|
||||
UPDATE expense_categories SET name = 'برمجيات وتراخيص', description = 'اشتراكات برامج وتراخيص' WHERE name = 'Software';
|
||||
UPDATE expense_categories SET name = 'مصروفات أخرى', description = 'نفقات متنوعة غير مصنفة' WHERE name = 'Other';
|
||||
23
db/migrations/024_link_expenses_to_accounting.sql
Normal file
23
db/migrations/024_link_expenses_to_accounting.sql
Normal file
@ -0,0 +1,23 @@
|
||||
-- Add account_id to expense_categories
|
||||
ALTER TABLE expense_categories ADD COLUMN account_id INT DEFAULT NULL;
|
||||
ALTER TABLE expense_categories ADD CONSTRAINT fk_expense_category_account FOREIGN KEY (account_id) REFERENCES accounting_accounts(id) ON DELETE SET NULL;
|
||||
|
||||
-- Seed default mappings (Best effort based on Arabic account names vs English categories)
|
||||
-- Accounts:
|
||||
-- 13: مصروفات الرواتب (Salaries)
|
||||
-- 16: مصروفات التسويق (Marketing)
|
||||
-- 30: مصروفات الإيجار (Rent)
|
||||
-- 31: مصروفات المرافق (Utilities)
|
||||
-- 12: تكلفة البضاعة المباعة (COGS)
|
||||
|
||||
-- Update 'Salaries' category
|
||||
UPDATE expense_categories SET account_id = 13 WHERE name LIKE '%Salaries%' OR name LIKE '%رواتب%';
|
||||
|
||||
-- Update 'Rent' category
|
||||
UPDATE expense_categories SET account_id = 30 WHERE name LIKE '%Rent%' OR name LIKE '%إيجار%';
|
||||
|
||||
-- Update 'Utilities' category
|
||||
UPDATE expense_categories SET account_id = 31 WHERE name LIKE '%Utilities%' OR name LIKE '%مرافق%';
|
||||
|
||||
-- Update 'Advertising' category
|
||||
UPDATE expense_categories SET account_id = 16 WHERE name LIKE '%Advertising%' OR name LIKE '%Marketing%' OR name LIKE '%تسو%';
|
||||
6
db/migrations/025_add_journal_tracking.sql
Normal file
6
db/migrations/025_add_journal_tracking.sql
Normal file
@ -0,0 +1,6 @@
|
||||
-- Add journal_id to expenses and hr_payroll for tracking
|
||||
ALTER TABLE expenses ADD COLUMN journal_id INT DEFAULT NULL;
|
||||
ALTER TABLE expenses ADD CONSTRAINT fk_expense_journal FOREIGN KEY (journal_id) REFERENCES accounting_journal(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE hr_payroll ADD COLUMN journal_id INT DEFAULT NULL;
|
||||
ALTER TABLE hr_payroll ADD CONSTRAINT fk_payroll_journal FOREIGN KEY (journal_id) REFERENCES accounting_journal(id) ON DELETE SET NULL;
|
||||
2
db/migrations/026_fix_accounting_accounts_type.sql
Normal file
2
db/migrations/026_fix_accounting_accounts_type.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- Fix accounting_accounts type column length
|
||||
ALTER TABLE accounting_accounts MODIFY type VARCHAR(100) NOT NULL;
|
||||
16
db/migrations/027_add_meeting_module.sql
Normal file
16
db/migrations/027_add_meeting_module.sql
Normal file
@ -0,0 +1,16 @@
|
||||
CREATE TABLE IF NOT EXISTS `meetings` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`title` VARCHAR(255) NOT NULL,
|
||||
`description` TEXT,
|
||||
`start_time` DATETIME NOT NULL,
|
||||
`end_time` DATETIME NOT NULL,
|
||||
`location` VARCHAR(255),
|
||||
`status` ENUM('scheduled', 'completed', 'cancelled') DEFAULT 'scheduled',
|
||||
`created_by` INT,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Add permissions for existing admins
|
||||
INSERT IGNORE INTO `user_permissions` (`user_id`, `page`, `can_view`, `can_add`, `can_edit`, `can_delete`)
|
||||
SELECT `id`, 'meetings', 1, 1, 1, 1 FROM `users` WHERE `role` = 'admin' OR `is_super_admin` = 1;
|
||||
5
db/migrations/028_expand_meetings_table.sql
Normal file
5
db/migrations/028_expand_meetings_table.sql
Normal file
@ -0,0 +1,5 @@
|
||||
ALTER TABLE `meetings`
|
||||
ADD COLUMN `agenda` TEXT AFTER `description`,
|
||||
ADD COLUMN `attendees` TEXT AFTER `location`,
|
||||
ADD COLUMN `absentees` TEXT AFTER `attendees`,
|
||||
ADD COLUMN `meeting_details` TEXT AFTER `absentees`;
|
||||
84
db/migrations/migrate.php
Normal file
84
db/migrations/migrate.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
function runMigrations() {
|
||||
$pdo = db();
|
||||
// Create migration table if not exists
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS migrations (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
migration_name VARCHAR(255) NOT NULL UNIQUE,
|
||||
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)");
|
||||
|
||||
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
|
||||
|
||||
$migrationsDir = __DIR__;
|
||||
$files = glob($migrationsDir . '/*.sql');
|
||||
sort($files);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$migration_name = basename($file);
|
||||
|
||||
// Check if already applied
|
||||
$stmt = $pdo->prepare("SELECT id FROM migrations WHERE migration_name = ?");
|
||||
$stmt->execute([$migration_name]);
|
||||
if ($stmt->fetch()) {
|
||||
echo "Already applied: $migration_name" . PHP_EOL;
|
||||
continue;
|
||||
}
|
||||
|
||||
echo "Applying: $migration_name" . PHP_EOL;
|
||||
$sql = file_get_contents($file);
|
||||
|
||||
// Split SQL into individual statements by ; followed by newline
|
||||
$statements = preg_split('/;(?:\\s*[\r\n]+)/', $sql);
|
||||
|
||||
$success = true;
|
||||
foreach ($statements as $statement) {
|
||||
$statement = trim($statement);
|
||||
if (empty($statement)) continue;
|
||||
|
||||
// Basic comment removal
|
||||
$statement_lines = explode("\n", $statement);
|
||||
$clean_statement = "";
|
||||
foreach ($statement_lines as $line) {
|
||||
if (trim(substr(trim($line), 0, 2)) === '--') continue;
|
||||
$clean_statement .= $line . "\n";
|
||||
}
|
||||
$clean_statement = trim($clean_statement);
|
||||
if (empty($clean_statement)) continue;
|
||||
|
||||
try {
|
||||
$stmt = $pdo->query($clean_statement);
|
||||
if ($stmt) {
|
||||
$stmt->closeCursor();
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
$msg = $e->getMessage();
|
||||
// If the error is about a table already existing, it's fine for our idempotency
|
||||
if (strpos($msg, "already exists") !== false ||
|
||||
strpos($msg, "Duplicate column") !== false ||
|
||||
strpos($msg, "Duplicate entry") !== false ||
|
||||
strpos($msg, "already a column") !== false ||
|
||||
strpos($msg, "Duplicate key") !== false) {
|
||||
continue;
|
||||
}
|
||||
echo "Error in $migration_name at statement: " . substr($clean_statement, 0, 100) . "... " . PHP_EOL;
|
||||
echo "Error: " . $msg . PHP_EOL;
|
||||
$success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
$stmt = $pdo->prepare("INSERT INTO migrations (migration_name) VALUES (?)");
|
||||
$stmt->execute([$migration_name]);
|
||||
echo "Successfully applied migration: $migration_name" . PHP_EOL;
|
||||
} else {
|
||||
echo "Migration failed: $migration_name. Stopping." . PHP_EOL;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runMigrations();
|
||||
219
expense_categories.php
Normal file
219
expense_categories.php
Normal file
@ -0,0 +1,219 @@
|
||||
<?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'] ?? '');
|
||||
$account_id = !empty($_POST['account_id']) ? $_POST['account_id'] : null;
|
||||
|
||||
if ($name) {
|
||||
try {
|
||||
$db = db();
|
||||
if ($action === 'add') {
|
||||
$stmt = $db->prepare("INSERT INTO expense_categories (name, description, account_id) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$name, $description, $account_id]);
|
||||
$_SESSION['success'] = 'تم إضافة التصنيف بنجاح';
|
||||
} elseif ($action === 'edit' && $id) {
|
||||
$stmt = $db->prepare("UPDATE expense_categories SET name = ?, description = ?, account_id = ? WHERE id = ?");
|
||||
$stmt->execute([$name, $description, $account_id, $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 c.*, a.name as account_name FROM expense_categories c LEFT JOIN accounting_accounts a ON c.account_id = a.id ORDER BY c.name")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$accounts = db()->query("SELECT * FROM accounting_accounts ORDER BY type, 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>الوصف</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>
|
||||
<?php if($cat['account_name']): ?>
|
||||
<span class="badge bg-info text-dark"><?= htmlspecialchars($cat['account_name']) ?></span>
|
||||
<?php else: ?>
|
||||
<span class="text-muted text-small">غير مرتبط</span>
|
||||
<?php endif; ?>
|
||||
</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>
|
||||
<select name="account_id" id="modalAccountId" class="form-select">
|
||||
<option value="">-- اختر الحساب --</option>
|
||||
<?php foreach($accounts as $acc): ?>
|
||||
<option value="<?= $acc['id'] ?>">
|
||||
<?= htmlspecialchars($acc['name']) ?> (<?= $acc['type'] ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-text">عند تسجيل مصروف بهذا التصنيف، سيتم إنشاء قيد محاسبي تلقائياً على هذا الحساب.</div>
|
||||
</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 = '';
|
||||
document.getElementById('modalAccountId').value = '';
|
||||
} else {
|
||||
title.textContent = 'تعديل التصنيف';
|
||||
document.getElementById('modalId').value = data.id;
|
||||
document.getElementById('modalName').value = data.name;
|
||||
document.getElementById('modalDescription').value = data.description;
|
||||
document.getElementById('modalAccountId').value = data.account_id || '';
|
||||
}
|
||||
|
||||
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
199
expense_reports.php
Normal 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, 3) ?> ر.ع</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, 3) ?></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'; ?>
|
||||
470
expenses.php
Normal file
470
expenses.php
Normal file
@ -0,0 +1,470 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
require_once __DIR__ . '/includes/accounting_functions.php'; // Include accounting helpers
|
||||
require_once __DIR__ . '/includes/pagination.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();
|
||||
$journal_id = null;
|
||||
|
||||
// --- Accounting Logic: Prepare Data ---
|
||||
// 1. Get Category Account
|
||||
$stmt_cat = $db->prepare("SELECT a.name as account_name FROM expense_categories c LEFT JOIN accounting_accounts a ON c.account_id = a.id WHERE c.id = ?");
|
||||
$stmt_cat->execute([$category_id]);
|
||||
$cat_account = $stmt_cat->fetchColumn();
|
||||
|
||||
// 2. Get Payment Account (Default: Cash / النقدية)
|
||||
// Ideally, map Payment Method to Account. For now, defaulting to 'النقدية'.
|
||||
$pay_account = 'النقدية';
|
||||
// Could be improved: if ($payment_method == 'Bank Transfer') $pay_account = 'Bank'; etc.
|
||||
|
||||
if ($action === 'add') {
|
||||
// Create Expense
|
||||
$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']]);
|
||||
$expense_id = $db->lastInsertId();
|
||||
|
||||
// Create Journal Entry
|
||||
if ($cat_account) {
|
||||
$entries = [
|
||||
['account' => $cat_account, 'debit' => $amount, 'credit' => 0],
|
||||
['account' => $pay_account, 'debit' => 0, 'credit' => $amount]
|
||||
];
|
||||
$journal_desc = "Expense #$expense_id: " . ($vendor ? "$vendor - " : "") . $description;
|
||||
$jid = add_journal_entry($date, $journal_desc, $reference, $entries);
|
||||
|
||||
if ($jid) {
|
||||
$db->prepare("UPDATE expenses SET journal_id = ? WHERE id = ?")->execute([$jid, $expense_id]);
|
||||
}
|
||||
}
|
||||
|
||||
$_SESSION['success'] = 'تم إضافة المصروف بنجاح' . ($cat_account ? ' وتم إنشاء قيد محاسبي.' : '');
|
||||
} else {
|
||||
// Update Expense
|
||||
// Get existing journal_id and file
|
||||
$stmt = $db->prepare("SELECT journal_id, receipt_file FROM expenses WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$journal_id = $row['journal_id'] ?? null;
|
||||
$old_file = $row['receipt_file'] ?? null;
|
||||
|
||||
if (!$receipt_path) $receipt_path = $old_file;
|
||||
|
||||
$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]);
|
||||
|
||||
// Update Journal Entry
|
||||
if ($cat_account) {
|
||||
$entries = [
|
||||
['account' => $cat_account, 'debit' => $amount, 'credit' => 0],
|
||||
['account' => $pay_account, 'debit' => 0, 'credit' => $amount]
|
||||
];
|
||||
$journal_desc = "Expense #$id: " . ($vendor ? "$vendor - " : "") . $description;
|
||||
|
||||
if ($journal_id) {
|
||||
edit_journal_entry($journal_id, $date, $journal_desc, $reference, $entries);
|
||||
} else {
|
||||
// Create new if missing
|
||||
$jid = add_journal_entry($date, $journal_desc, $reference, $entries);
|
||||
if ($jid) {
|
||||
$db->prepare("UPDATE expenses SET journal_id = ? WHERE id = ?")->execute([$jid, $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 and journal_id
|
||||
$stmt = $db->prepare("SELECT receipt_file, journal_id FROM expenses WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// Delete file
|
||||
if ($row && $row['receipt_file'] && file_exists($row['receipt_file'])) unlink($row['receipt_file']);
|
||||
|
||||
// Delete Journal Entry
|
||||
if ($row && $row['journal_id']) {
|
||||
delete_journal_entry($row['journal_id']);
|
||||
}
|
||||
|
||||
// Delete Expense
|
||||
$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'] ?? '';
|
||||
|
||||
// Build WHERE clause
|
||||
$whereConditions = ["e.date BETWEEN ? AND ?"];
|
||||
$params = [$date_from, $date_to];
|
||||
|
||||
if ($category_filter) {
|
||||
$whereConditions[] = "e.category_id = ?";
|
||||
$params[] = $category_filter;
|
||||
}
|
||||
if ($search) {
|
||||
$whereConditions[] = "(e.description LIKE ? OR e.vendor LIKE ? OR e.reference LIKE ?)";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
}
|
||||
|
||||
$whereClause = implode(' AND ', $whereConditions);
|
||||
|
||||
// Pagination
|
||||
$page = $_GET['page'] ?? 1;
|
||||
$perPage = 10;
|
||||
|
||||
// Count Total Items & Sum Total Amount
|
||||
$countSql = "SELECT COUNT(*) as count, SUM(amount) as total_amount FROM expenses e WHERE $whereClause";
|
||||
$countStmt = db()->prepare($countSql);
|
||||
$countStmt->execute($params);
|
||||
$countResult = $countStmt->fetch(PDO::FETCH_ASSOC);
|
||||
$totalExpenses = $countResult['count'];
|
||||
$grandTotalAmount = $countResult['total_amount'] ?? 0;
|
||||
|
||||
$pagination = getPagination($page, $totalExpenses, $perPage);
|
||||
|
||||
// Fetch Items
|
||||
$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 $whereClause
|
||||
ORDER BY e.date DESC, e.id DESC
|
||||
LIMIT ? OFFSET ?";
|
||||
|
||||
// Add LIMIT/OFFSET to params
|
||||
$params[] = $pagination['limit'];
|
||||
$params[] = $pagination['offset'];
|
||||
|
||||
$stmt = db()->prepare($sql);
|
||||
foreach ($params as $k => $v) {
|
||||
$type = is_int($v) ? PDO::PARAM_INT : PDO::PARAM_STR;
|
||||
$stmt->bindValue($k + 1, $v, $type);
|
||||
}
|
||||
$stmt->execute();
|
||||
$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; ?>
|
||||
<!-- Page Total -->
|
||||
<tr class="bg-light">
|
||||
<td colspan="4" class="text-end fw-bold">إجمالي الصفحة:</td>
|
||||
<td class="text-danger fw-bold"><?= number_format(array_sum(array_column($expenses, 'amount')), 2) ?></td>
|
||||
<td colspan="3"></td>
|
||||
</tr>
|
||||
<!-- Grand Total -->
|
||||
<tr class="bg-light border-top-0">
|
||||
<td colspan="4" class="text-end fw-bold">الإجمالي الكلي (للبحث الحالي):</td>
|
||||
<td class="text-danger fw-bold"><?= number_format($grandTotalAmount, 2) ?></td>
|
||||
<td colspan="3"></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-white">
|
||||
<?= renderPagination($pagination['current_page'], $pagination['total_pages']) ?>
|
||||
</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
228
expenses_dashboard.php
Normal 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'; ?>
|
||||
145
forgot_password.php
Normal file
145
forgot_password.php
Normal file
@ -0,0 +1,145 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
require_once __DIR__ . '/m_services/MailService.php';
|
||||
|
||||
if (isLoggedIn()) {
|
||||
redirect('index.php');
|
||||
}
|
||||
|
||||
$error = '';
|
||||
$success = '';
|
||||
$step = 'request'; // 'request' or 'reset'
|
||||
|
||||
// Check if we are in reset mode (token in URL)
|
||||
$token = $_GET['token'] ?? '';
|
||||
if ($token) {
|
||||
$stmt = db()->prepare("SELECT * FROM users WHERE reset_token = ? AND reset_token_expiry > NOW()");
|
||||
$stmt->execute([$token]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user) {
|
||||
$step = 'reset';
|
||||
} else {
|
||||
$error = 'رابط استعادة كلمة المرور غير صالح أو منتهي الصلاحية.';
|
||||
$step = 'request';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle POST requests
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_POST['request_reset'])) {
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$stmt = db()->prepare("SELECT id, full_name FROM users WHERE email = ?");
|
||||
$stmt->execute([$email]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user) {
|
||||
$newToken = bin2hex(random_bytes(32));
|
||||
$expiry = date('Y-m-d H:i:s', strtotime('+1 hour'));
|
||||
|
||||
$update = db()->prepare("UPDATE users SET reset_token = ?, reset_token_expiry = ? WHERE id = ?");
|
||||
$update->execute([$newToken, $expiry, $user['id']]);
|
||||
|
||||
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'];
|
||||
$resetLink = "$protocol://$host/forgot_password.php?token=$newToken";
|
||||
|
||||
$subject = "استعادة كلمة المرور - " . ($charity_info['charity_name'] ?? 'النظام');
|
||||
$html = "
|
||||
<div dir='rtl' style='font-family: Arial, sans-serif; text-align: right;'>
|
||||
<h3>مرحباً {$user['full_name']}</h3>
|
||||
<p>لقد طلبت استعادة كلمة المرور الخاصة بك. يرجى الضغط على الرابط أدناه لإعادة تعيينها:</p>
|
||||
<p><a href='$resetLink' style='background: #000; color: #fff; padding: 10px 20px; text-decoration: none; border-radius: 5px;'>إعادة تعيين كلمة المرور</a></p>
|
||||
<p>هذا الرابط صالح لمدة ساعة واحدة فقط.</p>
|
||||
<p>إذا لم تطلب هذا، يرجى تجاهل هذه الرسالة.</p>
|
||||
</div>
|
||||
";
|
||||
|
||||
$res = MailService::sendMail($email, $subject, $html);
|
||||
if ($res['success']) {
|
||||
$success = 'تم إرسال رابط استعادة كلمة المرور إلى بريدك الإلكتروني.';
|
||||
} else {
|
||||
$error = 'فشل إرسال البريد الإلكتروني. يرجى المحاولة لاحقاً أو التواصل مع الإدارة.';
|
||||
}
|
||||
} else {
|
||||
$error = 'البريد الإلكتروني غير مسجل لدينا.';
|
||||
}
|
||||
} else {
|
||||
$error = 'يرجى إدخال بريد إلكتروني صحيح.';
|
||||
}
|
||||
} elseif (isset($_POST['reset_password'])) {
|
||||
$password = $_POST['password'] ?? '';
|
||||
$confirm = $_POST['confirm_password'] ?? '';
|
||||
|
||||
if (strlen($password) < 6) {
|
||||
$error = 'كلمة المرور يجب أن تكون 6 أحرف على الأقل.';
|
||||
} elseif ($password !== $confirm) {
|
||||
$error = 'كلمات المرور غير متطابقة.';
|
||||
} else {
|
||||
$hashed = password_hash($password, PASSWORD_DEFAULT);
|
||||
$update = db()->prepare("UPDATE users SET password = ?, reset_token = NULL, reset_token_expiry = NULL WHERE id = ?");
|
||||
$update->execute([$hashed, $user['id']]);
|
||||
$success = 'تم تغيير كلمة المرور بنجاح. يمكنك الآن <a href="login.php">تسجيل الدخول</a>.';
|
||||
$step = 'completed';
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="row justify-content-center align-items-center" style="min-height: 80vh;">
|
||||
<div class="col-md-4 col-lg-3">
|
||||
<div class="card p-4 shadow-sm border-0 text-center">
|
||||
<div class="mb-4">
|
||||
<?php if (!empty($charity_info['charity_logo'])): ?>
|
||||
<img src="<?= htmlspecialchars($charity_info['charity_logo']) ?>" alt="Logo" class="mb-3" style="max-height: 80px;">
|
||||
<?php endif; ?>
|
||||
<h4 class="fw-bold">استعادة كلمة المرور</h4>
|
||||
<p class="text-muted small">بريد <?= htmlspecialchars($charity_info['charity_name'] ?? 'النظام') ?></p>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger"><?= $error ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?= $success ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($step === 'request'): ?>
|
||||
<form method="POST">
|
||||
<div class="mb-3 text-start">
|
||||
<label class="form-label">البريد الإلكتروني</label>
|
||||
<input type="email" name="email" class="form-control" placeholder="example@domain.com" required>
|
||||
<div class="form-text">أدخل البريد الإلكتروني المرتبط بحسابك.</div>
|
||||
</div>
|
||||
<div class="d-grid mt-4">
|
||||
<button type="submit" name="request_reset" class="btn btn-dark">إرسال رابط الاستعادة</button>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<a href="login.php" class="text-decoration-none small text-secondary">العودة لتسجيل الدخول</a>
|
||||
</div>
|
||||
</form>
|
||||
<?php elseif ($step === 'reset'): ?>
|
||||
<form method="POST">
|
||||
<div class="mb-3 text-start">
|
||||
<label class="form-label">كلمة المرور الجديدة</label>
|
||||
<input type="password" name="password" class="form-control" required minlength="6">
|
||||
</div>
|
||||
<div class="mb-3 text-start">
|
||||
<label class="form-label">تأكيد كلمة المرور</label>
|
||||
<input type="password" name="confirm_password" class="form-control" required minlength="6">
|
||||
</div>
|
||||
<div class="d-grid mt-4">
|
||||
<button type="submit" name="reset_password" class="btn btn-primary">تغيير كلمة المرور</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php elseif ($step === 'completed'): ?>
|
||||
<div class="mt-3">
|
||||
<a href="login.php" class="btn btn-outline-dark">الذهاب لصفحة الدخول</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
250
hr_attendance.php
Normal file
250
hr_attendance.php
Normal file
@ -0,0 +1,250 @@
|
||||
<?php
|
||||
require_once 'includes/header.php';
|
||||
require_once 'includes/pagination.php';
|
||||
|
||||
if (!canView('hr_attendance')) {
|
||||
echo "<div class='alert alert-danger'>ليس لديك صلاحية للوصول إلى هذه الصفحة.</div>";
|
||||
require_once 'includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$date = $_GET['date'] ?? date('Y-m-d');
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
// Handle Attendance Submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_attendance'])) {
|
||||
if (!canAdd('hr_attendance') && !canEdit('hr_attendance')) {
|
||||
$error = "لا تملك صلاحية التعديل.";
|
||||
} else {
|
||||
$emp_id = $_POST['employee_id'];
|
||||
$att_date = $_POST['date'];
|
||||
$status = $_POST['status'];
|
||||
$check_in = !empty($_POST['check_in']) ? $_POST['check_in'] : null;
|
||||
$check_out = !empty($_POST['check_out']) ? $_POST['check_out'] : null;
|
||||
$notes = $_POST['notes'];
|
||||
|
||||
try {
|
||||
// Check if exists
|
||||
$stmt = db()->prepare("SELECT id FROM hr_attendance WHERE employee_id = ? AND date = ?");
|
||||
$stmt->execute([$emp_id, $att_date]);
|
||||
$exists = $stmt->fetch();
|
||||
|
||||
if ($exists) {
|
||||
$stmt = db()->prepare("UPDATE hr_attendance SET status = ?, check_in = ?, check_out = ?, notes = ? WHERE id = ?");
|
||||
$stmt->execute([$status, $check_in, $check_out, $notes, $exists['id']]);
|
||||
$success = "تم تحديث الحضور بنجاح.";
|
||||
} else {
|
||||
$stmt = db()->prepare("INSERT INTO hr_attendance (employee_id, date, status, check_in, check_out, notes) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$emp_id, $att_date, $status, $check_in, $check_out, $notes]);
|
||||
$success = "تم تسجيل الحضور بنجاح.";
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
$error = "خطأ: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
if ($page < 1) $page = 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
// Count Total Employees
|
||||
$countStmt = db()->prepare("SELECT COUNT(*) FROM hr_employees WHERE status = 'active'");
|
||||
$countStmt->execute();
|
||||
$totalFiltered = $countStmt->fetchColumn();
|
||||
|
||||
// Fetch Employees and their attendance for the selected date
|
||||
$sql = "SELECT e.id, e.first_name, e.last_name, e.job_title,
|
||||
a.id as att_id, a.status, a.check_in, a.check_out, a.notes
|
||||
FROM hr_employees e
|
||||
LEFT JOIN hr_attendance a ON e.id = a.employee_id AND a.date = ?
|
||||
WHERE e.status = 'active'
|
||||
ORDER BY e.first_name
|
||||
LIMIT $limit OFFSET $offset";
|
||||
$stmt = db()->prepare($sql);
|
||||
$stmt->execute([$date]);
|
||||
$records = $stmt->fetchAll();
|
||||
|
||||
?>
|
||||
|
||||
<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">
|
||||
<form class="d-flex gap-2 align-items-center" method="get">
|
||||
<label class="col-form-label">التاريخ:</label>
|
||||
<input type="date" name="date" class="form-control" value="<?= $date ?>" onchange="this.form.submit()">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>الموظف</th>
|
||||
<th>الوظيفة</th>
|
||||
<th>الحالة</th>
|
||||
<th>وقت الحضور</th>
|
||||
<th>وقت الانصراف</th>
|
||||
<th>ملاحظات</th>
|
||||
<th>إجراء</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($records)): ?>
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4 text-muted">لا يوجد موظفين نشطين.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($records as $row): ?>
|
||||
<tr class="<?= !$row['att_id'] ? 'table-light text-muted' : '' ?>">
|
||||
<td class="fw-bold"><?= htmlspecialchars($row['first_name'] . ' ' . $row['last_name']) ?></td>
|
||||
<td class="small"><?= htmlspecialchars($row['job_title']) ?></td>
|
||||
<td>
|
||||
<?php if ($row['att_id']): ?>
|
||||
<?php
|
||||
$badge = match($row['status']) {
|
||||
'present' => 'success',
|
||||
'absent' => 'danger',
|
||||
'late' => 'warning',
|
||||
'excused' => 'info',
|
||||
'holiday' => 'primary',
|
||||
default => 'secondary'
|
||||
};
|
||||
$status_text = match($row['status']) {
|
||||
'present' => 'حاضر',
|
||||
'absent' => 'غائب',
|
||||
'late' => 'تأخير',
|
||||
'excused' => 'مأذون',
|
||||
'holiday' => 'عطلة',
|
||||
default => $row['status']
|
||||
};
|
||||
?>
|
||||
<span class="badge bg-<?= $badge ?>"><?= $status_text ?></span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-light text-dark border">غير مسجل</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= $row['check_in'] ? date('h:i A', strtotime($row['check_in'])) : '-' ?></td>
|
||||
<td><?= $row['check_out'] ? date('h:i A', strtotime($row['check_out'])) : '-' ?></td>
|
||||
<td class="text-truncate" style="max-width: 150px;"><?= htmlspecialchars($row['notes'] ?? '') ?></td>
|
||||
<td>
|
||||
<?php if (canEdit('hr_attendance')): ?>
|
||||
<?php if ($row['att_id']): ?>
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
title="تعديل"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#attModal"
|
||||
data-id="<?= $row['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($row['first_name'] . ' ' . $row['last_name']) ?>"
|
||||
data-status="<?= $row['status'] ?? 'present' ?>"
|
||||
data-in="<?= $row['check_in'] ?? '' ?>"
|
||||
data-out="<?= $row['check_out'] ?? '' ?>"
|
||||
data-notes="<?= htmlspecialchars($row['notes'] ?? '') ?>">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<button class="btn btn-sm btn-success"
|
||||
title="تسجيل حضور"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#attModal"
|
||||
data-id="<?= $row['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($row['first_name'] . ' ' . $row['last_name']) ?>"
|
||||
data-status="present"
|
||||
data-in="<?= date('09:00') ?>"
|
||||
data-out=""
|
||||
data-notes="">
|
||||
<i class="fas fa-check"></i> تسجيل
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php renderPagination($page, $totalFiltered, $limit); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attendance Modal -->
|
||||
<div class="modal fade" id="attModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">تسجيل/تعديل الحضور: <span id="modalEmpName" class="text-primary"></span></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form method="post">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="employee_id" id="modalEmpId">
|
||||
<input type="hidden" name="date" value="<?= $date ?>">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">الحالة</label>
|
||||
<select name="status" id="modalStatus" class="form-select" required>
|
||||
<option value="present">حاضر</option>
|
||||
<option value="late">تأخير</option>
|
||||
<option value="excused">مأذون/إذن</option>
|
||||
<option value="absent">غائب</option>
|
||||
<option value="holiday">عطلة</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col">
|
||||
<label class="form-label">وقت الحضور</label>
|
||||
<input type="time" name="check_in" id="modalIn" class="form-control">
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">وقت الانصراف</label>
|
||||
<input type="time" name="check_out" id="modalOut" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">ملاحظات</label>
|
||||
<textarea name="notes" id="modalNotes" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" name="save_attendance" class="btn btn-primary">حفظ</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const attModal = document.getElementById('attModal');
|
||||
if (attModal) {
|
||||
attModal.addEventListener('show.bs.modal', event => {
|
||||
const button = event.relatedTarget;
|
||||
|
||||
document.getElementById('modalEmpId').value = button.getAttribute('data-id');
|
||||
document.getElementById('modalEmpName').textContent = button.getAttribute('data-name');
|
||||
document.getElementById('modalStatus').value = button.getAttribute('data-status');
|
||||
document.getElementById('modalIn').value = button.getAttribute('data-in');
|
||||
document.getElementById('modalOut').value = button.getAttribute('data-out');
|
||||
document.getElementById('modalNotes').value = button.getAttribute('data-notes');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
153
hr_dashboard.php
Normal file
153
hr_dashboard.php
Normal file
@ -0,0 +1,153 @@
|
||||
<?php
|
||||
require_once 'includes/header.php';
|
||||
|
||||
if (!canView('hr_dashboard')) {
|
||||
echo "<div class='alert alert-danger'>ليس لديك صلاحية للوصول إلى هذه الصفحة.</div>";
|
||||
require_once 'includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Fetch Stats
|
||||
$total_employees = db()->query("SELECT COUNT(*) FROM hr_employees WHERE status = 'active'")->fetchColumn();
|
||||
$employees_present = db()->query("SELECT COUNT(*) FROM hr_attendance WHERE date = CURDATE() AND status = 'present'")->fetchColumn();
|
||||
$on_leave = db()->query("SELECT COUNT(*) FROM hr_leaves WHERE CURDATE() BETWEEN start_date AND end_date AND status = 'approved'")->fetchColumn();
|
||||
$pending_leaves = db()->query("SELECT COUNT(*) FROM hr_leaves WHERE status = 'pending'")->fetchColumn();
|
||||
|
||||
// Recent Employees
|
||||
$recent_employees = db()->query("SELECT * FROM hr_employees ORDER BY join_date DESC LIMIT 5")->fetchAll();
|
||||
?>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-primary shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="card-title mb-0">إجمالي الموظفين</h6>
|
||||
<h2 class="mt-2 mb-0"><?= number_format($total_employees) ?></h2>
|
||||
</div>
|
||||
<i class="fas fa-users fa-2x opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-success shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="card-title mb-0">حضور اليوم</h6>
|
||||
<h2 class="mt-2 mb-0"><?= number_format($employees_present) ?></h2>
|
||||
</div>
|
||||
<i class="fas fa-user-check fa-2x opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-warning shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="card-title mb-0">في إجازة</h6>
|
||||
<h2 class="mt-2 mb-0"><?= number_format($on_leave) ?></h2>
|
||||
</div>
|
||||
<i class="fas fa-plane-departure fa-2x opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-danger shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="card-title mb-0">طلبات إجازة معلقة</h6>
|
||||
<h2 class="mt-2 mb-0"><?= number_format($pending_leaves) ?></h2>
|
||||
</div>
|
||||
<i class="fas fa-clipboard-list fa-2x opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Recent Employees -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0"><i class="fas fa-user-plus me-2 text-primary"></i> أحدث الموظفين</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>الاسم</th>
|
||||
<th>الوظيفة</th>
|
||||
<th>تاريخ التعيين</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($recent_employees)): ?>
|
||||
<tr><td colspan="3" class="text-center py-3 text-muted">لا يوجد موظفين مسجلين حالياً</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($recent_employees as $emp): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($emp['first_name'] . ' ' . $emp['last_name']) ?></td>
|
||||
<td><?= htmlspecialchars($emp['job_title']) ?></td>
|
||||
<td><?= date('Y-m-d', strtotime($emp['join_date'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-white text-center">
|
||||
<a href="hr_employees.php" class="text-decoration-none small fw-bold">عرض كل الموظفين <i class="fas fa-arrow-left ms-1"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0"><i class="fas fa-bolt me-2 text-warning"></i> إجراءات سريعة</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-3">
|
||||
<?php if (canAdd('hr_employees')): ?>
|
||||
<a href="hr_employees.php?action=add" class="btn btn-outline-primary text-start">
|
||||
<i class="fas fa-plus-circle me-2"></i> إضافة موظف جديد
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if (canAdd('hr_attendance')): ?>
|
||||
<a href="hr_attendance.php?action=mark" class="btn btn-outline-success text-start">
|
||||
<i class="fas fa-clock me-2"></i> تسجيل حضور وانصراف
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if (canAdd('hr_leaves')): ?>
|
||||
<a href="hr_leaves.php?action=request" class="btn btn-outline-warning text-start">
|
||||
<i class="fas fa-calendar-plus me-2"></i> تقديم طلب إجازة
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if (canAdd('hr_payroll')): ?>
|
||||
<a href="hr_payroll.php?action=generate" class="btn btn-outline-info text-start">
|
||||
<i class="fas fa-file-invoice-dollar me-2"></i> إصدار مسير رواتب
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
354
hr_employees.php
Normal file
354
hr_employees.php
Normal file
@ -0,0 +1,354 @@
|
||||
<?php
|
||||
require_once 'includes/header.php';
|
||||
require_once __DIR__ . '/includes/pagination.php';
|
||||
|
||||
// Check Permission
|
||||
if (!canView('hr_employees')) {
|
||||
echo "<div class='alert alert-danger'>ليس لديك صلاحية للوصول إلى هذه الصفحة.</div>";
|
||||
require_once 'includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
// Handle Form Submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_POST['save_employee'])) {
|
||||
if (!canAdd('hr_employees') && !canEdit('hr_employees')) {
|
||||
$error = "لا تملك صلاحية التعديل.";
|
||||
} else {
|
||||
$id = !empty($_POST['id']) ? $_POST['id'] : null;
|
||||
$first_name = trim($_POST['first_name']);
|
||||
$last_name = trim($_POST['last_name']);
|
||||
$email = trim($_POST['email']);
|
||||
$phone = trim($_POST['phone']);
|
||||
$department_id = !empty($_POST['department_id']) ? $_POST['department_id'] : null;
|
||||
$job_title = trim($_POST['job_title']);
|
||||
$basic_salary = floatval($_POST['basic_salary']);
|
||||
$join_date = $_POST['join_date'];
|
||||
$status = $_POST['status'];
|
||||
$gender = $_POST['gender'];
|
||||
$birth_date = !empty($_POST['birth_date']) ? $_POST['birth_date'] : null;
|
||||
|
||||
if (empty($first_name) || empty($last_name) || empty($join_date)) {
|
||||
$error = "يرجى تعبئة الحقول الإلزامية.";
|
||||
} else {
|
||||
try {
|
||||
if ($id) {
|
||||
// Update
|
||||
$stmt = db()->prepare("UPDATE hr_employees SET first_name=?, last_name=?, email=?, phone=?, department_id=?, job_title=?, basic_salary=?, join_date=?, status=?, gender=?, birth_date=? WHERE id=?");
|
||||
$stmt->execute([$first_name, $last_name, $email, $phone, $department_id, $job_title, $basic_salary, $join_date, $status, $gender, $birth_date, $id]);
|
||||
$success = "تم تحديث بيانات الموظف بنجاح.";
|
||||
} else {
|
||||
// Insert
|
||||
$stmt = db()->prepare("INSERT INTO hr_employees (first_name, last_name, email, phone, department_id, job_title, basic_salary, join_date, status, gender, birth_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$first_name, $last_name, $email, $phone, $department_id, $job_title, $basic_salary, $join_date, $status, $gender, $birth_date]);
|
||||
$success = "تم إضافة الموظف بنجاح.";
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
$error = "خطأ في قاعدة البيانات: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif (isset($_POST['delete_employee'])) {
|
||||
if (!canDelete('hr_employees')) {
|
||||
$error = "لا تملك صلاحية الحذف.";
|
||||
} else {
|
||||
$id = $_POST['id'];
|
||||
try {
|
||||
$stmt = db()->prepare("DELETE FROM hr_employees WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$success = "تم حذف الموظف بنجاح.";
|
||||
} catch (PDOException $e) {
|
||||
$error = "لا يمكن حذف الموظف لوجود سجلات مرتبطة به.";
|
||||
}
|
||||
}
|
||||
} elseif (isset($_POST['save_department'])) {
|
||||
$dept_name = trim($_POST['name']);
|
||||
if (!empty($dept_name)) {
|
||||
$stmt = db()->prepare("INSERT INTO hr_departments (name) VALUES (?)");
|
||||
$stmt->execute([$dept_name]);
|
||||
$success = "تم إضافة القسم بنجاح.";
|
||||
}
|
||||
} elseif (isset($_POST['delete_department'])) {
|
||||
$dept_id = $_POST['id'];
|
||||
try {
|
||||
$stmt = db()->prepare("DELETE FROM hr_departments WHERE id = ?");
|
||||
$stmt->execute([$dept_id]);
|
||||
$success = "تم حذف القسم.";
|
||||
} catch (PDOException $e) {
|
||||
$error = "لا يمكن حذف القسم لأنه مرتبط بموظفين.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch Departments for Dropdown
|
||||
$departments = db()->query("SELECT * FROM hr_departments ORDER BY name")->fetchAll();
|
||||
|
||||
// Pagination
|
||||
$page = $_GET['page'] ?? 1;
|
||||
$perPage = 10;
|
||||
$totalEmployees = db()->query("SELECT COUNT(*) FROM hr_employees")->fetchColumn();
|
||||
$pagination = getPagination($page, $totalEmployees, $perPage);
|
||||
|
||||
?>
|
||||
|
||||
<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">
|
||||
<?php if (canAdd('hr_employees')): ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary me-2" data-bs-toggle="modal" data-bs-target="#deptModal">
|
||||
<i class="fas fa-building"></i> الأقسام
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#employeeModal" onclick="resetEmployeeForm()">
|
||||
<i class="fas fa-plus"></i> إضافة موظف
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Departments Modal -->
|
||||
<div class="modal fade" id="deptModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">إدارة الأقسام</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post" class="mb-3 d-flex gap-2">
|
||||
<input type="text" name="name" class="form-control" placeholder="اسم القسم الجديد" required>
|
||||
<button type="submit" name="save_department" class="btn btn-primary">إضافة</button>
|
||||
</form>
|
||||
<ul class="list-group">
|
||||
<?php foreach ($departments as $dept): ?>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<?= htmlspecialchars($dept['name']) ?>
|
||||
<form method="post" onsubmit="return confirm('هل أنت متأكد؟');">
|
||||
<input type="hidden" name="id" value="<?= $dept['id'] ?>">
|
||||
<button type="submit" name="delete_department" class="btn btn-sm btn-danger"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Employee Modal (Add/Edit) -->
|
||||
<div class="modal fade" id="employeeModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="employeeModalLabel">إضافة موظف جديد</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form method="post" id="employeeForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="id" id="empId">
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">الاسم الأول <span class="text-danger">*</span></label>
|
||||
<input type="text" name="first_name" id="empFirstName" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">اسم العائلة <span class="text-danger">*</span></label>
|
||||
<input type="text" name="last_name" id="empLastName" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">البريد الإلكتروني</label>
|
||||
<input type="email" name="email" id="empEmail" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">رقم الهاتف</label>
|
||||
<input type="text" name="phone" id="empPhone" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">الجنس</label>
|
||||
<select name="gender" id="empGender" class="form-select">
|
||||
<option value="male">ذكر</option>
|
||||
<option value="female">أنثى</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">تاريخ الميلاد</label>
|
||||
<input type="date" name="birth_date" id="empBirthDate" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">القسم</label>
|
||||
<select name="department_id" id="empDept" class="form-select">
|
||||
<option value="">-- اختر القسم --</option>
|
||||
<?php foreach ($departments as $dept): ?>
|
||||
<option value="<?= $dept['id'] ?>"><?= htmlspecialchars($dept['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">المسمى الوظيفي</label>
|
||||
<input type="text" name="job_title" id="empJobTitle" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">تاريخ التعيين <span class="text-danger">*</span></label>
|
||||
<input type="date" name="join_date" id="empJoinDate" class="form-control" value="<?= date('Y-m-d') ?>" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">الراتب الأساسي</label>
|
||||
<input type="number" step="0.01" name="basic_salary" id="empSalary" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">الحالة</label>
|
||||
<select name="status" id="empStatus" class="form-select">
|
||||
<option value="active">نشط</option>
|
||||
<option value="on_leave">في إجازة</option>
|
||||
<option value="resigned">مستقيل</option>
|
||||
<option value="terminated">منهي خدماته</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" name="save_employee" class="btn btn-primary">حفظ البيانات</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List View -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>الاسم</th>
|
||||
<th>القسم</th>
|
||||
<th>المسمى الوظيفي</th>
|
||||
<th>تاريخ التعيين</th>
|
||||
<th>الحالة</th>
|
||||
<th>الإجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$sql = "SELECT e.*, d.name as dept_name
|
||||
FROM hr_employees e
|
||||
LEFT JOIN hr_departments d ON e.department_id = d.id
|
||||
ORDER BY e.first_name
|
||||
LIMIT ? OFFSET ?";
|
||||
$stmt = db()->prepare($sql);
|
||||
$stmt->bindValue(1, $pagination['limit'], PDO::PARAM_INT);
|
||||
$stmt->bindValue(2, $pagination['offset'], PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
while ($row = $stmt->fetch()):
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 35px; height: 35px; font-size: 0.9rem;">
|
||||
<?= mb_substr($row['first_name'], 0, 1) . mb_substr($row['last_name'], 0, 1) ?>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold"><?= htmlspecialchars($row['first_name'] . ' ' . $row['last_name']) ?></div>
|
||||
<div class="small text-muted"><?= htmlspecialchars($row['email']) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="badge bg-secondary"><?= htmlspecialchars($row['dept_name'] ?? '-') ?></span></td>
|
||||
<td><?= htmlspecialchars($row['job_title']) ?></td>
|
||||
<td><?= $row['join_date'] ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$status_cls = match($row['status']) {
|
||||
'active' => 'success',
|
||||
'terminated' => 'danger',
|
||||
'resigned' => 'warning',
|
||||
'on_leave' => 'info',
|
||||
default => 'secondary'
|
||||
};
|
||||
?>
|
||||
<span class="badge bg-<?= $status_cls ?>"><?= htmlspecialchars($row['status']) ?></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<?php if (canEdit('hr_employees')): ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#employeeModal"
|
||||
data-id="<?= $row['id'] ?>"
|
||||
data-fname="<?= htmlspecialchars($row['first_name']) ?>"
|
||||
data-lname="<?= htmlspecialchars($row['last_name']) ?>"
|
||||
data-email="<?= htmlspecialchars($row['email']) ?>"
|
||||
data-phone="<?= htmlspecialchars($row['phone']) ?>"
|
||||
data-gender="<?= $row['gender'] ?>"
|
||||
data-bdate="<?= $row['birth_date'] ?>"
|
||||
data-dept="<?= $row['department_id'] ?>"
|
||||
data-job="<?= htmlspecialchars($row['job_title']) ?>"
|
||||
data-join="<?= $row['join_date'] ?>"
|
||||
data-salary="<?= $row['basic_salary'] ?>"
|
||||
data-status="<?= $row['status'] ?>"
|
||||
onclick="editEmployee(this)">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if (canDelete('hr_employees')): ?>
|
||||
<form method="post" onsubmit="return confirm('هل أنت متأكد من حذف هذا الموظف؟');" class="d-inline">
|
||||
<input type="hidden" name="id" value="<?= $row['id'] ?>">
|
||||
<button type="submit" name="delete_employee" class="btn btn-sm btn-outline-danger"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endwhile; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-white">
|
||||
<?= renderPagination($pagination['current_page'], $pagination['total_pages']) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function resetEmployeeForm() {
|
||||
document.getElementById('employeeForm').reset();
|
||||
document.getElementById('empId').value = '';
|
||||
document.getElementById('employeeModalLabel').textContent = 'إضافة موظف جديد';
|
||||
}
|
||||
|
||||
function editEmployee(btn) {
|
||||
document.getElementById('employeeModalLabel').textContent = 'تعديل بيانات موظف';
|
||||
document.getElementById('empId').value = btn.dataset.id;
|
||||
document.getElementById('empFirstName').value = btn.dataset.fname;
|
||||
document.getElementById('empLastName').value = btn.dataset.lname;
|
||||
document.getElementById('empEmail').value = btn.dataset.email;
|
||||
document.getElementById('empPhone').value = btn.dataset.phone;
|
||||
document.getElementById('empGender').value = btn.dataset.gender;
|
||||
document.getElementById('empBirthDate').value = btn.dataset.bdate;
|
||||
document.getElementById('empDept').value = btn.dataset.dept;
|
||||
document.getElementById('empJobTitle').value = btn.dataset.job;
|
||||
document.getElementById('empJoinDate').value = btn.dataset.join;
|
||||
document.getElementById('empSalary').value = btn.dataset.salary;
|
||||
document.getElementById('empStatus').value = btn.dataset.status;
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
183
hr_holidays.php
Normal file
183
hr_holidays.php
Normal file
@ -0,0 +1,183 @@
|
||||
<?php
|
||||
require_once 'includes/header.php';
|
||||
|
||||
if (!canView('hr_attendance')) {
|
||||
echo "<div class='alert alert-danger'>ليس لديك صلاحية للوصول إلى هذه الصفحة.</div>";
|
||||
require_once 'includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_POST['save_holiday'])) {
|
||||
if (!canAdd('hr_attendance') && !canEdit('hr_attendance')) {
|
||||
$error = "لا تملك صلاحية التعديل.";
|
||||
} else {
|
||||
$id = $_POST['id'] ?? null;
|
||||
$name = trim($_POST['name']);
|
||||
$from = $_POST['date_from'];
|
||||
$to = $_POST['date_to'];
|
||||
|
||||
if (!empty($name) && !empty($from) && !empty($to)) {
|
||||
if ($id) {
|
||||
$stmt = db()->prepare("UPDATE hr_holidays SET name=?, date_from=?, date_to=? WHERE id=?");
|
||||
$stmt->execute([$name, $from, $to, $id]);
|
||||
$success = "تم تحديث العطلة بنجاح.";
|
||||
} else {
|
||||
$stmt = db()->prepare("INSERT INTO hr_holidays (name, date_from, date_to) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$name, $from, $to]);
|
||||
$success = "تم إضافة العطلة بنجاح.";
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif (isset($_POST['delete_holiday'])) {
|
||||
if (!canDelete('hr_attendance')) {
|
||||
$error = "لا تملك صلاحية الحذف.";
|
||||
} else {
|
||||
$id = $_POST['id'];
|
||||
$stmt = db()->prepare("DELETE FROM hr_holidays WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$success = "تم حذف العطلة.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$holidays = db()->query("SELECT * FROM hr_holidays ORDER BY date_from DESC")->fetchAll();
|
||||
?>
|
||||
|
||||
<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">
|
||||
<?php if (canAdd('hr_attendance')): ?>
|
||||
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#holidayModal" onclick="resetHolidayForm()">
|
||||
<i class="fas fa-plus"></i> إضافة عطلة
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>اسم العطلة</th>
|
||||
<th>من تاريخ</th>
|
||||
<th>إلى تاريخ</th>
|
||||
<th>الحالة</th>
|
||||
<th>إجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($holidays)): ?>
|
||||
<tr><td colspan="5" class="text-center py-4 text-muted">لا توجد عطلات مسجلة.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($holidays as $row):
|
||||
$today = date('Y-m-d');
|
||||
$status_cls = 'secondary';
|
||||
$status_txt = 'منتهية';
|
||||
|
||||
if ($today >= $row['date_from'] && $today <= $row['date_to']) {
|
||||
$status_cls = 'success';
|
||||
$status_txt = 'جارية';
|
||||
} elseif ($today < $row['date_from']) {
|
||||
$status_cls = 'primary';
|
||||
$status_txt = 'قادمة';
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td class="fw-bold"><?= htmlspecialchars($row['name']) ?></td>
|
||||
<td><?= $row['date_from'] ?></td>
|
||||
<td><?= $row['date_to'] ?></td>
|
||||
<td><span class="badge bg-<?= $status_cls ?>"><?= $status_txt ?></span></td>
|
||||
<td>
|
||||
<?php if (canEdit('hr_attendance')): ?>
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#holidayModal"
|
||||
data-id="<?= $row['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($row['name']) ?>"
|
||||
data-from="<?= $row['date_from'] ?>"
|
||||
data-to="<?= $row['date_to'] ?>"
|
||||
onclick="editHoliday(this)">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if (canDelete('hr_attendance')): ?>
|
||||
<form method="post" onsubmit="return confirm('هل أنت متأكد؟');" class="d-inline">
|
||||
<input type="hidden" name="id" value="<?= $row['id'] ?>">
|
||||
<button type="submit" name="delete_holiday" class="btn btn-sm btn-outline-danger"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Holiday Modal -->
|
||||
<div class="modal fade" id="holidayModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="holidayModalTitle">إضافة عطلة جديدة</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form method="post" id="holidayForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="id" id="holidayId">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">اسم العطلة</label>
|
||||
<input type="text" name="name" id="holidayName" class="form-control" required placeholder="مثال: عيد الفطر">
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col">
|
||||
<label class="form-label">من تاريخ</label>
|
||||
<input type="date" name="date_from" id="holidayFrom" class="form-control" required>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">إلى تاريخ</label>
|
||||
<input type="date" name="date_to" id="holidayTo" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" name="save_holiday" class="btn btn-primary">حفظ</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function resetHolidayForm() {
|
||||
document.getElementById('holidayForm').reset();
|
||||
document.getElementById('holidayId').value = '';
|
||||
document.getElementById('holidayModalTitle').textContent = 'إضافة عطلة جديدة';
|
||||
}
|
||||
|
||||
function editHoliday(btn) {
|
||||
document.getElementById('holidayModalTitle').textContent = 'تعديل عطلة';
|
||||
document.getElementById('holidayId').value = btn.getAttribute('data-id');
|
||||
document.getElementById('holidayName').value = btn.getAttribute('data-name');
|
||||
document.getElementById('holidayFrom').value = btn.getAttribute('data-from');
|
||||
document.getElementById('holidayTo').value = btn.getAttribute('data-to');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
292
hr_leaves.php
Normal file
292
hr_leaves.php
Normal file
@ -0,0 +1,292 @@
|
||||
<?php
|
||||
require_once 'includes/header.php';
|
||||
require_once 'includes/pagination.php';
|
||||
|
||||
if (!canView('hr_leaves')) {
|
||||
echo "<div class='alert alert-danger'>ليس لديك صلاحية للوصول إلى هذه الصفحة.</div>";
|
||||
require_once 'includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$tab = $_GET['tab'] ?? 'pending';
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
// Handle Form Submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_POST['request_leave'])) {
|
||||
if (!canAdd('hr_leaves')) {
|
||||
$error = "لا تملك صلاحية الإضافة.";
|
||||
} else {
|
||||
$id = $_POST['id'] ?? null; // For edit
|
||||
$emp_id = $_POST['employee_id'];
|
||||
$type = $_POST['leave_type'];
|
||||
$start = $_POST['start_date'];
|
||||
$end = $_POST['end_date'];
|
||||
$reason = trim($_POST['reason']);
|
||||
|
||||
$start_dt = new DateTime($start);
|
||||
$end_dt = new DateTime($end);
|
||||
$days = $end_dt->diff($start_dt)->days + 1;
|
||||
|
||||
if ($days <= 0) {
|
||||
$error = "تاريخ النهاية يجب أن يكون بعد تاريخ البداية.";
|
||||
} else {
|
||||
try {
|
||||
if ($id) {
|
||||
// Update existing request
|
||||
$stmt = db()->prepare("UPDATE hr_leaves SET employee_id=?, leave_type=?, start_date=?, end_date=?, days_count=?, reason=? WHERE id=? AND status='pending'");
|
||||
$stmt->execute([$emp_id, $type, $start, $end, $days, $reason, $id]);
|
||||
$success = "تم تحديث طلب الإجازة بنجاح.";
|
||||
} else {
|
||||
// New request
|
||||
$stmt = db()->prepare("INSERT INTO hr_leaves (employee_id, leave_type, start_date, end_date, days_count, reason, status) VALUES (?, ?, ?, ?, ?, ?, 'pending')");
|
||||
$stmt->execute([$emp_id, $type, $start, $end, $days, $reason]);
|
||||
$success = "تم تقديم طلب الإجازة بنجاح.";
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
$error = "خطأ: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif (isset($_POST['update_status'])) {
|
||||
if (!canEdit('hr_leaves')) {
|
||||
$error = "لا تملك صلاحية الاعتماد.";
|
||||
} else {
|
||||
$id = $_POST['id'];
|
||||
$status = $_POST['status'];
|
||||
$stmt = db()->prepare("UPDATE hr_leaves SET status = ?, approved_by = ? WHERE id = ?");
|
||||
$stmt->execute([$status, $_SESSION['user_id'], $id]);
|
||||
$success = "تم تحديث حالة الطلب.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch Employees for Dropdown
|
||||
$employees = db()->query("SELECT id, first_name, last_name FROM hr_employees WHERE status = 'active' ORDER BY first_name")->fetchAll();
|
||||
|
||||
// Pagination
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
if ($page < 1) $page = 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
// Fetch Leaves based on Tab
|
||||
$where_clause = $tab === 'pending' ? "WHERE l.status = 'pending'" : "WHERE 1=1";
|
||||
|
||||
// Count Total
|
||||
$countSql = "SELECT COUNT(*) FROM hr_leaves l $where_clause";
|
||||
$countStmt = db()->query($countSql);
|
||||
$totalFiltered = $countStmt->fetchColumn();
|
||||
|
||||
$sql = "SELECT l.*, e.first_name, e.last_name, u.full_name as approver_name
|
||||
FROM hr_leaves l
|
||||
JOIN hr_employees e ON l.employee_id = e.id
|
||||
LEFT JOIN users u ON l.approved_by = u.id
|
||||
$where_clause
|
||||
ORDER BY l.created_at DESC
|
||||
LIMIT $limit OFFSET $offset";
|
||||
$requests = db()->query($sql)->fetchAll();
|
||||
|
||||
?>
|
||||
|
||||
<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">
|
||||
<?php if (canAdd('hr_leaves')): ?>
|
||||
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#leaveModal" onclick="resetLeaveForm()">
|
||||
<i class="fas fa-plus"></i> طلب إجازة جديد
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<ul class="nav nav-tabs mb-4">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $tab === 'pending' ? 'active' : '' ?>" href="?tab=pending">الطلبات المعلقة</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $tab === 'all' ? 'active' : '' ?>" href="?tab=all">سجل الإجازات</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>الموظف</th>
|
||||
<th>نوع الإجازة</th>
|
||||
<th>الفترة</th>
|
||||
<th>المدة</th>
|
||||
<th>السبب</th>
|
||||
<th>الحالة</th>
|
||||
<?php if ($tab === 'all'): ?><th>المعتمد</th><?php endif; ?>
|
||||
<th>إجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($requests)): ?>
|
||||
<tr><td colspan="8" class="text-center py-4 text-muted">لا توجد طلبات.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($requests as $req): ?>
|
||||
<tr>
|
||||
<td class="fw-bold"><?= htmlspecialchars($req['first_name'] . ' ' . $req['last_name']) ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$type_map = [
|
||||
'annual' => 'سنوية',
|
||||
'sick' => 'مرضية',
|
||||
'unpaid' => 'بدون راتب',
|
||||
'maternity' => 'أمومة',
|
||||
'emergency' => 'طارئة',
|
||||
'other' => 'أخرى'
|
||||
];
|
||||
echo $type_map[$req['leave_type']] ?? $req['leave_type'];
|
||||
?>
|
||||
</td>
|
||||
<td class="small">
|
||||
من <?= $req['start_date'] ?><br>إلى <?= $req['end_date'] ?>
|
||||
</td>
|
||||
<td><?= $req['days_count'] ?> يوم</td>
|
||||
<td class="text-truncate" style="max-width: 150px;"><?= htmlspecialchars($req['reason']) ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$status_cls = match($req['status']) {
|
||||
'approved' => 'success',
|
||||
'rejected' => 'danger',
|
||||
default => 'warning'
|
||||
};
|
||||
$status_txt = match($req['status']) {
|
||||
'approved' => 'مقبولة',
|
||||
'rejected' => 'مرفوضة',
|
||||
default => 'معلقة'
|
||||
};
|
||||
?>
|
||||
<span class="badge bg-<?= $status_cls ?>"><?= $status_txt ?></span>
|
||||
</td>
|
||||
<?php if ($tab === 'all'): ?>
|
||||
<td class="small"><?= htmlspecialchars($req['approver_name'] ?? '-') ?></td>
|
||||
<?php endif; ?>
|
||||
<td>
|
||||
<?php if ($req['status'] === 'pending' && canEdit('hr_leaves')): ?>
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
title="تعديل الطلب"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#leaveModal"
|
||||
data-id="<?= $req['id'] ?>"
|
||||
data-emp="<?= $req['employee_id'] ?>"
|
||||
data-type="<?= $req['leave_type'] ?>"
|
||||
data-start="<?= $req['start_date'] ?>"
|
||||
data-end="<?= $req['end_date'] ?>"
|
||||
data-reason="<?= htmlspecialchars($req['reason']) ?>"
|
||||
onclick="editLeave(this)">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
|
||||
<form method="post" class="d-inline">
|
||||
<input type="hidden" name="id" value="<?= $req['id'] ?>">
|
||||
<input type="hidden" name="status" value="approved">
|
||||
<button type="submit" name="update_status" class="btn btn-sm btn-success" title="قبول"><i class="fas fa-check"></i></button>
|
||||
</form>
|
||||
<form method="post" class="d-inline">
|
||||
<input type="hidden" name="id" value="<?= $req['id'] ?>">
|
||||
<input type="hidden" name="status" value="rejected">
|
||||
<button type="submit" name="update_status" class="btn btn-sm btn-danger" title="رفض" onclick="return confirm('هل أنت متأكد من الرفض؟')"><i class="fas fa-times"></i></button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php renderPagination($page, $totalFiltered, $limit); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leave Request Modal -->
|
||||
<div class="modal fade" id="leaveModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="leaveModalTitle">تقديم طلب إجازة</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form method="post" id="leaveForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="id" id="leaveId">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">الموظف</label>
|
||||
<select name="employee_id" id="leaveEmp" class="form-select" required>
|
||||
<option value="">-- اختر الموظف --</option>
|
||||
<?php foreach ($employees as $emp): ?>
|
||||
<option value="<?= $emp['id'] ?>"><?= htmlspecialchars($emp['first_name'] . ' ' . $emp['last_name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">نوع الإجازة</label>
|
||||
<select name="leave_type" id="leaveType" class="form-select" required>
|
||||
<option value="annual">سنوية</option>
|
||||
<option value="sick">مرضية</option>
|
||||
<option value="emergency">طارئة</option>
|
||||
<option value="unpaid">بدون راتب</option>
|
||||
<option value="maternity">أمومة</option>
|
||||
<option value="other">أخرى</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col">
|
||||
<label class="form-label">من تاريخ</label>
|
||||
<input type="date" name="start_date" id="leaveStart" class="form-control" required>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">إلى تاريخ</label>
|
||||
<input type="date" name="end_date" id="leaveEnd" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">السبب</label>
|
||||
<textarea name="reason" id="leaveReason" class="form-control" rows="3" placeholder="سبب الإجازة..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" name="request_leave" class="btn btn-primary">حفظ الطلب</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function resetLeaveForm() {
|
||||
document.getElementById('leaveForm').reset();
|
||||
document.getElementById('leaveId').value = '';
|
||||
document.getElementById('leaveModalTitle').textContent = 'تقديم طلب إجازة';
|
||||
}
|
||||
|
||||
function editLeave(btn) {
|
||||
document.getElementById('leaveModalTitle').textContent = 'تعديل طلب إجازة';
|
||||
document.getElementById('leaveId').value = btn.getAttribute('data-id');
|
||||
document.getElementById('leaveEmp').value = btn.getAttribute('data-emp');
|
||||
document.getElementById('leaveType').value = btn.getAttribute('data-type');
|
||||
document.getElementById('leaveStart').value = btn.getAttribute('data-start');
|
||||
document.getElementById('leaveEnd').value = btn.getAttribute('data-end');
|
||||
document.getElementById('leaveReason').value = btn.getAttribute('data-reason');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
332
hr_payroll.php
Normal file
332
hr_payroll.php
Normal file
@ -0,0 +1,332 @@
|
||||
<?php
|
||||
require_once 'includes/header.php';
|
||||
require_once 'includes/accounting_functions.php'; // Include accounting helpers
|
||||
require_once 'includes/pagination.php';
|
||||
|
||||
if (!canView('hr_payroll')) {
|
||||
echo "<div class='alert alert-danger'>ليس لديك صلاحية للوصول إلى هذه الصفحة.</div>";
|
||||
require_once 'includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$month = $_GET['month'] ?? date('m');
|
||||
$year = $_GET['year'] ?? date('Y');
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
// Handle Payroll Actions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if (isset($_POST['generate_payroll'])) {
|
||||
if (!canAdd('hr_payroll')) {
|
||||
$error = "لا تملك صلاحية التوليد.";
|
||||
} else {
|
||||
$gen_month = $_POST['month'];
|
||||
$gen_year = $_POST['year'];
|
||||
|
||||
// Get all active employees
|
||||
$employees = db()->query("SELECT id, basic_salary FROM hr_employees WHERE status = 'active'")->fetchAll();
|
||||
$count = 0;
|
||||
|
||||
foreach ($employees as $emp) {
|
||||
// Check if already exists
|
||||
$stmt = db()->prepare("SELECT id FROM hr_payroll WHERE employee_id = ? AND month = ? AND year = ?");
|
||||
$stmt->execute([$emp['id'], $gen_month, $gen_year]);
|
||||
if ($stmt->fetch()) continue; // Skip if exists
|
||||
|
||||
// Calculate Absent Deductions
|
||||
$stmt = db()->prepare("SELECT COUNT(*) FROM hr_attendance WHERE employee_id = ? AND status = 'absent' AND MONTH(date) = ? AND YEAR(date) = ?");
|
||||
$stmt->execute([$emp['id'], $gen_month, $gen_year]);
|
||||
$absent_days = $stmt->fetchColumn();
|
||||
|
||||
$daily_rate = $emp['basic_salary'] / 30;
|
||||
$deductions = round($absent_days * $daily_rate, 2);
|
||||
$net = $emp['basic_salary'] - $deductions;
|
||||
|
||||
$stmt = db()->prepare("INSERT INTO hr_payroll (employee_id, month, year, basic_salary, deductions, net_salary, status) VALUES (?, ?, ?, ?, ?, ?, 'pending')");
|
||||
$stmt->execute([$emp['id'], $gen_month, $gen_year, $emp['basic_salary'], $deductions, $net]);
|
||||
$count++;
|
||||
}
|
||||
$success = "تم توليد الرواتب لـ $count موظف.";
|
||||
}
|
||||
} elseif (isset($_POST['update_payroll'])) {
|
||||
if (!canEdit('hr_payroll')) {
|
||||
$error = "لا تملك صلاحية التعديل.";
|
||||
} else {
|
||||
$id = $_POST['id'];
|
||||
$bonuses = floatval($_POST['bonuses']);
|
||||
$deductions = floatval($_POST['deductions']);
|
||||
$status = $_POST['status'];
|
||||
|
||||
// Recalculate Net
|
||||
$stmt = db()->prepare("SELECT * FROM hr_payroll WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$current = $stmt->fetch();
|
||||
|
||||
if ($current) {
|
||||
$net = $current['basic_salary'] + $bonuses - $deductions;
|
||||
$payment_date = ($status == 'paid') ? date('Y-m-d') : null;
|
||||
$journal_id = $current['journal_id'];
|
||||
|
||||
try {
|
||||
$db = db();
|
||||
// Update Payroll Record
|
||||
$stmt = $db->prepare("UPDATE hr_payroll SET bonuses = ?, deductions = ?, net_salary = ?, status = ?, payment_date = ? WHERE id = ?");
|
||||
$stmt->execute([$bonuses, $deductions, $net, $status, $payment_date, $id]);
|
||||
|
||||
// Accounting Integration
|
||||
if ($status == 'paid') {
|
||||
// Create or Update Journal Entry
|
||||
$salary_account = 'مصروفات الرواتب'; // Account ID 13
|
||||
$cash_account = 'النقدية'; // Account ID 17
|
||||
|
||||
$entries = [
|
||||
['account' => $salary_account, 'debit' => $net, 'credit' => 0],
|
||||
['account' => $cash_account, 'debit' => 0, 'credit' => $net]
|
||||
];
|
||||
|
||||
// Get Employee Name for Description
|
||||
$stmt_emp = $db->prepare("SELECT first_name, last_name FROM hr_employees WHERE id = ?");
|
||||
$stmt_emp->execute([$current['employee_id']]);
|
||||
$emp_name = $stmt_emp->fetch(PDO::FETCH_ASSOC);
|
||||
$emp_full_name = $emp_name ? ($emp_name['first_name'] . ' ' . $emp_name['last_name']) : "Employee #{$current['employee_id']}";
|
||||
|
||||
$description = "Salary Payment: $emp_full_name ({$current['month']}/{$current['year']})";
|
||||
$reference = "PAY-{$current['year']}-{$current['month']}-{$current['employee_id']}";
|
||||
|
||||
if ($journal_id) {
|
||||
edit_journal_entry($journal_id, $payment_date, $description, $reference, $entries);
|
||||
} else {
|
||||
$jid = add_journal_entry($payment_date, $description, $reference, $entries);
|
||||
if ($jid) {
|
||||
$db->prepare("UPDATE hr_payroll SET journal_id = ? WHERE id = ?")->execute([$jid, $id]);
|
||||
}
|
||||
}
|
||||
} elseif ($status == 'pending' && $journal_id) {
|
||||
// If changing back to pending, delete the journal entry
|
||||
delete_journal_entry($journal_id);
|
||||
$db->prepare("UPDATE hr_payroll SET journal_id = NULL WHERE id = ?")->execute([$id]);
|
||||
}
|
||||
|
||||
$success = "تم تحديث الراتب.";
|
||||
} catch (Exception $e) {
|
||||
$error = "حدث خطأ: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
if ($page < 1) $page = 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
// Fetch Payroll Records
|
||||
$sql = "SELECT p.*, e.first_name, e.last_name, e.job_title
|
||||
FROM hr_payroll p
|
||||
JOIN hr_employees e ON p.employee_id = e.id
|
||||
WHERE p.month = ? AND p.year = ?
|
||||
ORDER BY e.first_name
|
||||
LIMIT $limit OFFSET $offset";
|
||||
$stmt = db()->prepare($sql);
|
||||
$stmt->execute([$month, $year]);
|
||||
$payrolls = $stmt->fetchAll();
|
||||
|
||||
// Count Total for Pagination
|
||||
$countSql = "SELECT COUNT(*) FROM hr_payroll WHERE month = ? AND year = ?";
|
||||
$countStmt = db()->prepare($countSql);
|
||||
$countStmt->execute([$month, $year]);
|
||||
$totalFiltered = $countStmt->fetchColumn();
|
||||
|
||||
// Calculate Grand Total Salaries (for the stats card)
|
||||
$sumSql = "SELECT SUM(net_salary) FROM hr_payroll WHERE month = ? AND year = ?";
|
||||
$sumStmt = db()->prepare($sumSql);
|
||||
$sumStmt->execute([$month, $year]);
|
||||
$total_salaries = $sumStmt->fetchColumn() ?: 0;
|
||||
|
||||
?>
|
||||
|
||||
<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">
|
||||
<form class="d-flex gap-2 align-items-center" method="get">
|
||||
<select name="month" class="form-select form-select-sm">
|
||||
<?php for($m=1; $m<=12; $m++): ?>
|
||||
<option value="<?= $m ?>" <?= $m == $month ? 'selected' : '' ?>><?= date('F', mktime(0, 0, 0, $m, 1)) ?></option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
<select name="year" class="form-select form-select-sm">
|
||||
<?php for($y=date('Y')-1; $y<=date('Y')+1; $y++): ?>
|
||||
<option value="<?= $y ?>" <?= $y == $year ? 'selected' : '' ?>><?= $y ?></option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary">عرض</button>
|
||||
</form>
|
||||
|
||||
<?php if (canAdd('hr_payroll')): ?>
|
||||
<button type="button" class="btn btn-sm btn-primary ms-2" data-bs-toggle="modal" data-bs-target="#generateModal">
|
||||
<i class="fas fa-cog"></i> توليد الرواتب
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body py-2 text-center">
|
||||
<h6 class="mb-0 text-muted">إجمالي الرواتب للشهر</h6>
|
||||
<h4 class="mb-0 text-primary fw-bold"><?= number_format($total_salaries, 2) ?></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>الموظف</th>
|
||||
<th>الراتب الأساسي</th>
|
||||
<th>إضافي</th>
|
||||
<th>خصومات</th>
|
||||
<th>الصافي</th>
|
||||
<th>الحالة</th>
|
||||
<th>إجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($payrolls)): ?>
|
||||
<tr><td colspan="7" class="text-center py-4 text-muted">لا توجد بيانات لهذا الشهر. اضغط على "توليد الرواتب" للبدء.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($payrolls as $row): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-bold"><?= htmlspecialchars($row['first_name'] . ' ' . $row['last_name']) ?></div>
|
||||
<div class="small text-muted"><?= htmlspecialchars($row['job_title']) ?></div>
|
||||
</td>
|
||||
<td><?= number_format($row['basic_salary'], 2) ?></td>
|
||||
<td class="text-success"><?= number_format($row['bonuses'], 2) ?></td>
|
||||
<td class="text-danger"><?= number_format($row['deductions'], 2) ?></td>
|
||||
<td class="fw-bold"><?= number_format($row['net_salary'], 2) ?></td>
|
||||
<td>
|
||||
<span class="badge bg-<?= $row['status'] == 'paid' ? 'success' : 'warning' ?>">
|
||||
<?= $row['status'] == 'paid' ? 'مدفوع' : 'معلق' ?>
|
||||
</span>
|
||||
<?php if($row['journal_id']): ?>
|
||||
<span class="badge bg-info text-dark" title="مرحل للمحاسبة"><i class="fas fa-check-circle"></i></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (canEdit('hr_payroll')): ?>
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#editPayModal"
|
||||
data-id="<?= $row['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($row['first_name']) ?>"
|
||||
data-bonus="<?= $row['bonuses'] ?>"
|
||||
data-deduct="<?= $row['deductions'] ?>"
|
||||
data-status="<?= $row['status'] ?>">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php renderPagination($page, $totalFiltered, $limit); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generate Modal -->
|
||||
<div class="modal fade" id="generateModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">توليد الرواتب</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form method="post">
|
||||
<div class="modal-body">
|
||||
<p>سيتم حساب الرواتب لجميع الموظفين النشطين لشهر: <strong><?= $month ?> / <?= $year ?></strong></p>
|
||||
<p class="text-muted small">سيتم احتساب الخصومات تلقائياً بناءً على أيام الغياب المسجلة.</p>
|
||||
<input type="hidden" name="month" value="<?= $month ?>">
|
||||
<input type="hidden" name="year" value="<?= $year ?>">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" name="generate_payroll" class="btn btn-primary">تأكيد التوليد</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Payroll Modal -->
|
||||
<div class="modal fade" id="editPayModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">تعديل الراتب: <span id="payName"></span></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form method="post">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="id" id="payId">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">مكافآت / إضافي</label>
|
||||
<input type="number" step="0.01" name="bonuses" id="payBonus" class="form-control">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">خصومات</label>
|
||||
<input type="number" step="0.01" name="deductions" id="payDeduct" class="form-control">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">الحالة</label>
|
||||
<select name="status" id="payStatus" class="form-select">
|
||||
<option value="pending">معلق</option>
|
||||
<option value="paid">مدفوع</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" name="update_payroll" class="btn btn-primary">حفظ التغييرات</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const editPayModal = document.getElementById('editPayModal');
|
||||
if (editPayModal) {
|
||||
editPayModal.addEventListener('show.bs.modal', event => {
|
||||
const button = event.relatedTarget;
|
||||
document.getElementById('payId').value = button.getAttribute('data-id');
|
||||
document.getElementById('payName').textContent = button.getAttribute('data-name');
|
||||
document.getElementById('payBonus').value = button.getAttribute('data-bonus');
|
||||
document.getElementById('payDeduct').value = button.getAttribute('data-deduct');
|
||||
document.getElementById('payStatus').value = button.getAttribute('data-status');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
170
hr_reports.php
Normal file
170
hr_reports.php
Normal file
@ -0,0 +1,170 @@
|
||||
<?php
|
||||
require_once 'includes/header.php';
|
||||
|
||||
if (!canView('hr_reports')) {
|
||||
echo "<div class='alert alert-danger'>ليس لديك صلاحية للوصول إلى هذه الصفحة.</div>";
|
||||
require_once 'includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$report_type = $_GET['type'] ?? 'attendance_summary';
|
||||
$month = $_GET['month'] ?? date('m');
|
||||
$year = $_GET['year'] ?? date('Y');
|
||||
|
||||
?>
|
||||
|
||||
<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 d-print-none">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="window.print()">
|
||||
<i class="fas fa-print"></i> طباعة
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row d-print-none mb-4">
|
||||
<div class="col-md-12">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $report_type == 'attendance_summary' ? 'active' : '' ?>" href="?type=attendance_summary">ملخص الحضور الشهري</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $report_type == 'employee_list' ? 'active' : '' ?>" href="?type=employee_list">قائمة الموظفين الشاملة</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<?php if ($report_type == 'attendance_summary'): ?>
|
||||
<div class="card shadow-sm mb-4 d-print-none">
|
||||
<div class="card-body">
|
||||
<form class="row g-3 align-items-end" method="get">
|
||||
<input type="hidden" name="type" value="attendance_summary">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">الشهر</label>
|
||||
<select name="month" class="form-select">
|
||||
<?php for($m=1; $m<=12; $m++): ?>
|
||||
<option value="<?= $m ?>" <?= $m == $month ? 'selected' : '' ?>><?= date('F', mktime(0, 0, 0, $m, 1)) ?></option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">السنة</label>
|
||||
<select name="year" class="form-select">
|
||||
<?php for($y=date('Y')-1; $y<=date('Y')+1; $y++): ?>
|
||||
<option value="<?= $y ?>" <?= $y == $year ? 'selected' : '' ?>><?= $y ?></option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-primary w-100">عرض التقرير</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">تقرير الحضور لشهر <?= $month ?> / <?= $year ?></h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-start">الموظف</th>
|
||||
<th>أيام الحضور</th>
|
||||
<th>أيام الغياب</th>
|
||||
<th>التأخير</th>
|
||||
<th>إجازات</th>
|
||||
<th>نسبة الحضور</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$sql = "SELECT e.id, e.first_name, e.last_name,
|
||||
COUNT(CASE WHEN a.status = 'present' THEN 1 END) as present_days,
|
||||
COUNT(CASE WHEN a.status = 'absent' THEN 1 END) as absent_days,
|
||||
COUNT(CASE WHEN a.status = 'late' THEN 1 END) as late_days,
|
||||
COUNT(CASE WHEN a.status = 'excused' OR a.status = 'holiday' THEN 1 END) as leave_days
|
||||
FROM hr_employees e
|
||||
LEFT JOIN hr_attendance a ON e.id = a.employee_id
|
||||
AND MONTH(a.date) = ? AND YEAR(a.date) = ?
|
||||
WHERE e.status = 'active'
|
||||
GROUP BY e.id
|
||||
ORDER BY e.first_name";
|
||||
$stmt = db()->prepare($sql);
|
||||
$stmt->execute([$month, $year]);
|
||||
$report_data = $stmt->fetchAll();
|
||||
|
||||
foreach ($report_data as $row):
|
||||
$total_days_recorded = $row['present_days'] + $row['absent_days'] + $row['late_days'] + $row['leave_days'];
|
||||
$attendance_rate = $total_days_recorded > 0
|
||||
? round((($row['present_days'] + $row['late_days']) / $total_days_recorded) * 100, 1)
|
||||
: 0;
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-start fw-bold"><?= htmlspecialchars($row['first_name'] . ' ' . $row['last_name']) ?></td>
|
||||
<td class="text-success"><?= $row['present_days'] ?></td>
|
||||
<td class="text-danger"><?= $row['absent_days'] ?></td>
|
||||
<td class="text-warning"><?= $row['late_days'] ?></td>
|
||||
<td class="text-info"><?= $row['leave_days'] ?></td>
|
||||
<td class="fw-bold"><?= $attendance_rate ?>%</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php elseif ($report_type == 'employee_list'): ?>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">سجل الموظفين الشامل</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>الرقم الوظيفي</th>
|
||||
<th>الاسم</th>
|
||||
<th>القسم</th>
|
||||
<th>الوظيفة</th>
|
||||
<th>تاريخ التعيين</th>
|
||||
<th>الراتب الأساسي</th>
|
||||
<th>رقم الهاتف</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$employees = db()->query("SELECT e.*, d.name as dept_name FROM hr_employees e LEFT JOIN hr_departments d ON e.department_id = d.id WHERE e.status = 'active' ORDER BY e.id")->fetchAll();
|
||||
foreach ($employees as $emp):
|
||||
?>
|
||||
<tr>
|
||||
<td>#<?= $emp['id'] ?></td>
|
||||
<td><?= htmlspecialchars($emp['first_name'] . ' ' . $emp['last_name']) ?></td>
|
||||
<td><?= htmlspecialchars($emp['dept_name'] ?? '-') ?></td>
|
||||
<td><?= htmlspecialchars($emp['job_title']) ?></td>
|
||||
<td><?= $emp['join_date'] ?></td>
|
||||
<td><?= number_format($emp['basic_salary'], 2) ?></td>
|
||||
<td><?= htmlspecialchars($emp['phone']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<style>
|
||||
@media print {
|
||||
.d-print-none { display: none !important; }
|
||||
.sidebar, .top-navbar { display: none !important; }
|
||||
.main-content { margin: 0 !important; padding: 0 !important; }
|
||||
.card { border: none !important; shadow: none !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
557
inbound.php
Normal file
557
inbound.php
Normal file
@ -0,0 +1,557 @@
|
||||
<?php
|
||||
require_once 'includes/header.php';
|
||||
require_once 'includes/pagination.php';
|
||||
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
// Handle CRUD operations
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
if (!canEdit('inbound') && !canAdd('inbound')) {
|
||||
$error = 'ليس لديك صلاحية للقيام بهذا الإجراء.';
|
||||
} else {
|
||||
$action = $_POST['action'];
|
||||
$id = $_POST['id'] ?? 0;
|
||||
$ref_no = $_POST['ref_no'] ?? '';
|
||||
$date_registered = $_POST['date_registered'] ?? date('Y-m-d');
|
||||
$due_date = !empty($_POST['due_date']) ? $_POST['due_date'] : null;
|
||||
$sender = $_POST['sender'] ?? '';
|
||||
$recipient = $_POST['recipient'] ?? '';
|
||||
$subject = $_POST['subject'] ?? '';
|
||||
$description = $_POST['description'] ?? '';
|
||||
$status_id = !empty($_POST['status_id']) ? $_POST['status_id'] : null;
|
||||
$assigned_to = !empty($_POST['assigned_to']) ? $_POST['assigned_to'] : null;
|
||||
|
||||
if ($action === 'add' || $action === 'edit') {
|
||||
try {
|
||||
db()->beginTransaction();
|
||||
|
||||
if ($action === 'add') {
|
||||
if (!canAdd('inbound')) throw new Exception('ليس لديك صلاحية الإضافة.');
|
||||
$stmt = db()->prepare("INSERT INTO inbound_mail (ref_no, date_registered, due_date, sender, recipient, subject, description, status_id, assigned_to, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$ref_no, $date_registered, $due_date, $sender, $recipient, $subject, $description, $status_id, $assigned_to, $_SESSION['user_id']]);
|
||||
$id = db()->lastInsertId();
|
||||
$success = 'تم إضافة البريد الوارد بنجاح.';
|
||||
} else {
|
||||
if (!canEdit('inbound')) throw new Exception('ليس لديك صلاحية التعديل.');
|
||||
$stmt = db()->prepare("UPDATE inbound_mail SET ref_no = ?, date_registered = ?, due_date = ?, sender = ?, recipient = ?, subject = ?, description = ?, status_id = ?, assigned_to = ? WHERE id = ?");
|
||||
$stmt->execute([$ref_no, $date_registered, $due_date, $sender, $recipient, $subject, $description, $status_id, $assigned_to, $id]);
|
||||
$success = 'تم تحديث بيانات البريد الوارد بنجاح.';
|
||||
}
|
||||
|
||||
// Handle file uploads
|
||||
if (isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0])) {
|
||||
$upload_dir = 'uploads/attachments/';
|
||||
if (!is_dir($upload_dir)) mkdir($upload_dir, 0777, true);
|
||||
|
||||
for ($i = 0; $i < count($_FILES['attachments']['name']); $i++) {
|
||||
if ($_FILES['attachments']['error'][$i] === 0) {
|
||||
$filename = time() . '_' . $_FILES['attachments']['name'][$i];
|
||||
$filepath = $upload_dir . $filename;
|
||||
if (move_uploaded_file($_FILES['attachments']['tmp_name'][$i], $filepath)) {
|
||||
$stmt = db()->prepare("INSERT INTO inbound_attachments (mail_id, display_name, file_path, file_name, file_size) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$id, $_FILES['attachments']['name'][$i], $filepath, $_FILES['attachments']['name'][$i], $_FILES['attachments']['size'][$i]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db()->commit();
|
||||
} catch (Exception $e) {
|
||||
db()->rollBack();
|
||||
$error = 'خطأ: ' . $e->getMessage();
|
||||
}
|
||||
} elseif ($action === 'delete') {
|
||||
if (!canDelete('inbound')) {
|
||||
$error = 'ليس لديك صلاحية الحذف.';
|
||||
} else {
|
||||
$stmt = db()->prepare("DELETE FROM inbound_mail WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$success = 'تم حذف البريد الوارد بنجاح.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch stats
|
||||
$total_stmt = db()->query("SELECT COUNT(*) FROM inbound_mail");
|
||||
$total_inbound = $total_stmt->fetchColumn();
|
||||
|
||||
$pending_stmt = db()->prepare("SELECT COUNT(*) FROM inbound_mail WHERE status_id IN (SELECT id FROM mailbox_statuses WHERE is_default = 1 OR name LIKE '%قيد%')");
|
||||
$pending_stmt->execute();
|
||||
$pending_inbound = $pending_stmt->fetchColumn();
|
||||
|
||||
// Search and Filter
|
||||
$where = "WHERE 1=1";
|
||||
$params = [];
|
||||
|
||||
if (isset($_GET['search']) && !empty($_GET['search'])) {
|
||||
$where .= " AND (m.ref_no LIKE ? OR m.subject LIKE ? OR m.sender LIKE ? OR m.recipient LIKE ?)";
|
||||
$search = "%" . $_GET['search'] . "%";
|
||||
$params = array_merge($params, [$search, $search, $search, $search]);
|
||||
}
|
||||
|
||||
if (isset($_GET['status_id']) && !empty($_GET['status_id'])) {
|
||||
$where .= " AND m.status_id = ?";
|
||||
$params[] = $_GET['status_id'];
|
||||
}
|
||||
|
||||
if (isset($_GET['my_tasks'])) {
|
||||
$where .= " AND m.assigned_to = ?";
|
||||
$params[] = $_SESSION['user_id'];
|
||||
}
|
||||
|
||||
// Pagination
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
if ($page < 1) $page = 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
// Count filtered
|
||||
$countQuery = "SELECT COUNT(*) FROM inbound_mail m $where";
|
||||
$countStmt = db()->prepare($countQuery);
|
||||
$countStmt->execute($params);
|
||||
$totalFiltered = $countStmt->fetchColumn();
|
||||
|
||||
$query = "SELECT m.*, s.name as status_name, s.color as status_color, u.full_name as assigned_to_name,
|
||||
(SELECT GROUP_CONCAT(display_name SEPARATOR '|||') FROM inbound_attachments WHERE mail_id = m.id) as attachment_names
|
||||
FROM inbound_mail m
|
||||
LEFT JOIN mailbox_statuses s ON m.status_id = s.id
|
||||
LEFT JOIN users u ON m.assigned_to = u.id
|
||||
$where
|
||||
ORDER BY m.date_registered DESC, m.id DESC
|
||||
LIMIT $limit OFFSET $offset";
|
||||
|
||||
$stmt = db()->prepare($query);
|
||||
$stmt->execute($params);
|
||||
$mails = $stmt->fetchAll();
|
||||
|
||||
$statuses = db()->query("SELECT * FROM mailbox_statuses ORDER BY id ASC")->fetchAll();
|
||||
$users = db()->query("SELECT id, full_name, username FROM users ORDER BY full_name ASC")->fetchAll();
|
||||
$default_status_id = db()->query("SELECT id FROM mailbox_statuses WHERE is_default = 1 LIMIT 1")->fetchColumn() ?: ($statuses[0]['id'] ?? null);
|
||||
|
||||
$deepLinkData = null;
|
||||
if (isset($_GET['id'])) {
|
||||
$dlStmt = db()->prepare("SELECT m.*, (SELECT GROUP_CONCAT(display_name SEPARATOR '|||') FROM inbound_attachments WHERE mail_id = m.id) as attachment_names FROM inbound_mail m WHERE m.id = ?");
|
||||
$dlStmt->execute([$_GET['id']]);
|
||||
$deepLinkData = $dlStmt->fetch();
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<h2 class="fw-bold mb-1"><i class="fas fa-download text-primary me-2"></i> البريد الوارد</h2>
|
||||
<p class="text-muted">إدارة جميع المراسلات الواردة والمهام المسندة.</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-md-end d-flex align-items-center justify-content-md-end gap-2">
|
||||
<?php if (canAdd('inbound')): ?>
|
||||
<button class="btn btn-primary px-4 py-2" onclick="openMailModal('add')">
|
||||
<i class="fas fa-plus me-1"></i> إضافة بريد جديد
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle bg-primary bg-opacity-10 p-3 me-3">
|
||||
<i class="fas fa-inbox text-primary fa-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="card-subtitle text-muted mb-1 small">إجمالي الوارد</h6>
|
||||
<h3 class="card-title mb-0 fw-bold"><?= $total_inbound ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle bg-warning bg-opacity-10 p-3 me-3">
|
||||
<i class="fas fa-clock text-warning fa-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="card-subtitle text-muted mb-1 small">قيد المعالجة</h6>
|
||||
<h3 class="card-title mb-0 fw-bold"><?= $pending_inbound ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger shadow-sm border-0 mb-4"><?= $error ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success shadow-sm border-0 mb-4"><?= $success ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-3">
|
||||
<form method="GET" class="row g-2 align-items-center">
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-white border-end-0"><i class="fas fa-search text-muted"></i></span>
|
||||
<input type="text" name="search" class="form-control border-start-0" placeholder="بحث برقم القيد، الموضوع، أو المرسل..." value="<?= htmlspecialchars($_GET['search'] ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="status_id" class="form-select" onchange="this.form.submit()">
|
||||
<option value="">جميع الحالات</option>
|
||||
<?php foreach ($statuses as $status): ?>
|
||||
<option value="<?= $status['id'] ?>" <?= (isset($_GET['status_id']) && $_GET['status_id'] == $status['id']) ? 'selected' : '' ?>><?= $status['name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" name="my_tasks" id="myTasks" <?= isset($_GET['my_tasks']) ? 'checked' : '' ?> onchange="this.form.submit()">
|
||||
<label class="form-check-label ms-2" for="myTasks">مهامي فقط</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-end">
|
||||
<button type="submit" class="btn btn-light w-100">تصفية</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mails Table -->
|
||||
<div class="card border-0 shadow-sm overflow-hidden">
|
||||
<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 class="text-center pe-4">الإجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($mails)): ?>
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-5 text-muted">
|
||||
<i class="fas fa-inbox fa-3x mb-3 opacity-20"></i>
|
||||
<p>لا يوجد بريد وارد حالياً.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($mails as $mail): ?>
|
||||
<tr>
|
||||
<td class="ps-4"><span class="fw-bold text-primary"><?= htmlspecialchars($mail['ref_no']) ?></span></td>
|
||||
<td><?= date('Y-m-d', strtotime($mail['date_registered'])) ?></td>
|
||||
<td>
|
||||
<div class="fw-semibold text-truncate" style="max-width: 250px;"><?= htmlspecialchars($mail['subject']) ?></div>
|
||||
<?php if ($mail['attachment_names']): ?>
|
||||
<span class="badge bg-light text-muted fw-normal" style="font-size: 0.65rem;">
|
||||
<i class="fas fa-paperclip me-1"></i> <?= count(explode('|||', $mail['attachment_names'])) ?> مرفقات
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($mail['sender']) ?></td>
|
||||
<td>
|
||||
<span class="badge rounded-pill" style="background-color: <?= $mail['status_color'] ?>20; color: <?= $mail['status_color'] ?>;">
|
||||
<i class="fas fa-circle me-1 small"></i> <?= htmlspecialchars($mail['status_name']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
|
||||
<i class="fas fa-user-circle text-muted small"></i>
|
||||
</div>
|
||||
<span class="small"><?= htmlspecialchars($mail['assigned_to_name'] ?: 'غير محدد') ?></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center pe-4">
|
||||
<div class="btn-group shadow-sm rounded">
|
||||
<a href="view_mail.php?id=<?= $mail['id'] ?>&type=inbound" class="btn btn-sm btn-white text-primary border" title="عرض">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="print_inbound.php?id=<?= $mail['id'] ?>" target="_blank" class="btn btn-sm btn-white text-secondary border" title="طباعة">
|
||||
<i class="fas fa-print"></i>
|
||||
</a>
|
||||
<?php if (canEdit('inbound')): ?>
|
||||
<button class="btn btn-sm btn-white text-warning border" onclick='openMailModal("edit", <?= json_encode($mail) ?>)' title="تعديل">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if (canDelete('inbound')): ?>
|
||||
<form method="POST" class="d-inline" onsubmit="return confirm('هل أنت متأكد من حذف هذا البريد؟');">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="id" value="<?= $mail['id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-white text-danger border rounded-0" title="حذف">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php renderPagination($page, $totalFiltered, $limit); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Modal -->
|
||||
<div class="modal fade" id="mailModal" tabindex="-1" aria-labelledby="mailModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header bg-primary text-white py-3">
|
||||
<h5 class="modal-title fw-bold" id="mailModalLabel">بريد وارد جديد</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="mailForm" method="POST" enctype="multipart/form-data">
|
||||
<div class="modal-body p-4">
|
||||
<input type="hidden" name="action" id="modalAction" value="add">
|
||||
<input type="hidden" name="id" id="modalId" value="0">
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">رقم القيد</label>
|
||||
<input type="text" name="ref_no" id="modalRefNo" class="form-control" readonly required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">تاريخ التسجيل</label>
|
||||
<input type="date" name="date_registered" id="modalDateRegistered" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">الجهة المرسلة</label>
|
||||
<input type="text" name="sender" id="modalSender" class="form-control" required placeholder="مثال: وزارة العدل">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">الجهة المستلمة (الداخلية)</label>
|
||||
<input type="text" name="recipient" id="modalRecipient" class="form-control" required placeholder="مثال: مكتب المدير العام">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label small fw-bold">الموضوع</label>
|
||||
<input type="text" name="subject" id="modalSubject" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label small fw-bold">الوصف والملاحظات</label>
|
||||
<textarea name="description" id="modalDescription" class="form-control" rows="5"></textarea>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">تاريخ الاستحقاق (اختياري)</label>
|
||||
<input type="date" name="due_date" id="modalDueDate" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">الحالة</label>
|
||||
<select name="status_id" id="modalStatusId" class="form-select" required>
|
||||
<?php foreach ($statuses as $status): ?>
|
||||
<option value="<?= $status['id'] ?>"><?= $status['name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label small fw-bold">إسناد المهمة إلى</label>
|
||||
<select name="assigned_to" id="modalAssignedTo" class="form-select">
|
||||
<option value="">--- اختر مستخدم ---</option>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<option value="<?= $user['id'] ?>"><?= htmlspecialchars($user['full_name'] ?: $user['username']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-12 mt-4">
|
||||
<div class="bg-light p-3 rounded">
|
||||
<label class="form-label small fw-bold mb-2 d-block">المرفقات</label>
|
||||
<input type="file" name="attachments[]" id="modalAttachmentsInput" class="form-control" multiple>
|
||||
<div id="modalExistingAttachments" class="mt-2"></div>
|
||||
<div id="modalSelectedAttachments" class="mt-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light border-0">
|
||||
<button type="button" class="btn btn-white border px-4" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary px-4">حفظ البيانات</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Custom Table Styles */
|
||||
.table thead th {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05rem;
|
||||
font-weight: 700;
|
||||
color: #6c757d;
|
||||
border-top: none;
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
.btn-white {
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
}
|
||||
.btn-white:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.badge {
|
||||
padding: 0.5em 0.8em;
|
||||
font-weight: 500;
|
||||
}
|
||||
.input-group-text {
|
||||
color: #adb5bd;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let mailModal;
|
||||
|
||||
function initEditors() {
|
||||
if (typeof tinymce === 'undefined') {
|
||||
console.error('TinyMCE not loaded');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return tinymce.init({
|
||||
selector: '#modalDescription',
|
||||
language: 'ar', language_url: 'https://cdn.jsdelivr.net/npm/tinymce-i18n@23.10.9/langs6/ar.js',
|
||||
directionality: 'rtl',
|
||||
height: 300,
|
||||
plugins: 'advlist autolink lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table help wordcount',
|
||||
toolbar: 'undo redo | fontfamily fontsize | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | help',
|
||||
font_size_formats: '8pt 10pt 12pt 14pt 18pt 24pt 36pt',
|
||||
promotion: false,
|
||||
branding: false
|
||||
});
|
||||
}
|
||||
|
||||
function openMailModal(action, data = null) {
|
||||
if (!mailModal) {
|
||||
const modalEl = document.getElementById('mailModal');
|
||||
if (typeof bootstrap !== 'undefined') {
|
||||
mailModal = new bootstrap.Modal(modalEl);
|
||||
} else {
|
||||
console.error('Bootstrap not loaded');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const label = document.getElementById('mailModalLabel');
|
||||
const modalAction = document.getElementById('modalAction');
|
||||
const modalId = document.getElementById('modalId');
|
||||
const existingAttachmentsDiv = document.getElementById('modalExistingAttachments');
|
||||
const selectedAttachmentsDiv = document.getElementById('modalSelectedAttachments');
|
||||
const attachmentsInput = document.getElementById('modalAttachmentsInput');
|
||||
|
||||
const fields = {
|
||||
ref_no: document.getElementById('modalRefNo'),
|
||||
date_registered: document.getElementById('modalDateRegistered'),
|
||||
due_date: document.getElementById('modalDueDate'),
|
||||
sender: document.getElementById('modalSender'),
|
||||
recipient: document.getElementById('modalRecipient'),
|
||||
subject: document.getElementById('modalSubject'),
|
||||
status_id: document.getElementById('modalStatusId'),
|
||||
assigned_to: document.getElementById('modalAssignedTo')
|
||||
};
|
||||
|
||||
modalAction.value = action;
|
||||
existingAttachmentsDiv.innerHTML = '';
|
||||
selectedAttachmentsDiv.innerHTML = '';
|
||||
if (attachmentsInput) attachmentsInput.value = '';
|
||||
|
||||
if (action === 'add') {
|
||||
label.textContent = 'إضافة بريد وارد جديد';
|
||||
modalId.value = '0';
|
||||
Object.keys(fields).forEach(key => {
|
||||
if (fields[key]) {
|
||||
if (key === 'date_registered') fields[key].value = '<?= date('Y-m-d') ?>';
|
||||
else if (key === 'status_id') fields[key].value = '<?= $default_status_id ?>';
|
||||
else if (key === 'ref_no') fields[key].value = '<?= generateRefNo('inbound') ?>';
|
||||
else fields[key].value = '';
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof tinymce !== 'undefined' && tinymce.get('modalDescription')) {
|
||||
tinymce.get('modalDescription').setContent('');
|
||||
} else {
|
||||
document.getElementById('modalDescription').value = '';
|
||||
}
|
||||
} else {
|
||||
label.textContent = 'تعديل البريد الوارد';
|
||||
modalId.value = data.id;
|
||||
Object.keys(fields).forEach(key => {
|
||||
if (fields[key]) fields[key].value = data[key] || '';
|
||||
});
|
||||
|
||||
if (typeof tinymce !== 'undefined' && tinymce.get('modalDescription')) {
|
||||
tinymce.get('modalDescription').setContent(data.description || '');
|
||||
} else {
|
||||
document.getElementById('modalDescription').value = data.description || '';
|
||||
}
|
||||
|
||||
// Display existing attachments
|
||||
if (data.attachment_names) {
|
||||
const names = data.attachment_names.split('|||');
|
||||
let html = '<div class="mt-2"><p class="mb-1 fw-bold small">المرفقات الحالية:</p><ul class="list-unstyled small">';
|
||||
names.forEach(name => {
|
||||
html += `<li><i class="fas fa-file-alt me-1 text-muted"></i> ${name}</li>`;
|
||||
});
|
||||
html += '</ul></div>';
|
||||
existingAttachmentsDiv.innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
mailModal.show();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initEditors().finally(() => {
|
||||
<?php if ($deepLinkData): ?>
|
||||
openMailModal('edit', <?= json_encode($deepLinkData) ?>);
|
||||
<?php elseif ($error && isset($_POST['action'])): ?>
|
||||
const errorData = <?= json_encode($_POST) ?>;
|
||||
openMailModal(errorData.action, errorData);
|
||||
<?php endif; ?>
|
||||
});
|
||||
|
||||
// Handle file selection display
|
||||
const attachmentsInput = document.getElementById('modalAttachmentsInput');
|
||||
if (attachmentsInput) {
|
||||
attachmentsInput.addEventListener('change', function() {
|
||||
const fileList = this.files;
|
||||
const selectedAttachmentsDiv = document.getElementById('modalSelectedAttachments');
|
||||
selectedAttachmentsDiv.innerHTML = '';
|
||||
|
||||
if (fileList.length > 0) {
|
||||
let html = '<div class="mt-2"><p class="mb-1 fw-bold small text-primary">المرفقات المختارة للرفع:</p><ul class="list-unstyled small">';
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const fileSize = (fileList[i].size / 1024).toFixed(1);
|
||||
html += `<li><i class="fas fa-file-upload me-1 text-primary"></i> ${fileList[i].name} <span class="text-muted">(${fileSize} KB)</span></li>`;
|
||||
}
|
||||
html += '</ul></div>';
|
||||
selectedAttachmentsDiv.innerHTML = html;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('mailForm').addEventListener('submit', function() {
|
||||
if (typeof tinymce !== 'undefined') {
|
||||
tinymce.triggerSave();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
233
includes/accounting_functions.php
Normal file
233
includes/accounting_functions.php
Normal file
@ -0,0 +1,233 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
function get_journal_entries() {
|
||||
$db = db();
|
||||
$stmt = $db->query("SELECT j.*, SUM(e.debit) as total_debit, SUM(e.credit) as total_credit
|
||||
FROM accounting_journal j
|
||||
LEFT JOIN accounting_entries e ON j.id = e.journal_id
|
||||
GROUP BY j.id ORDER BY j.date DESC");
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
function get_full_ledger() {
|
||||
$db = db();
|
||||
$stmt = $db->query("SELECT j.id, j.date, j.description, j.reference, e.account_name, e.debit, e.credit
|
||||
FROM accounting_journal j
|
||||
JOIN accounting_entries e ON j.id = e.journal_id
|
||||
ORDER BY j.date DESC, j.id DESC");
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
function get_full_ledger_filtered($search = '', $date_from = '', $date_to = '') {
|
||||
$db = db();
|
||||
$sql = "SELECT j.id, j.date, j.description, j.reference, e.account_name, e.debit, e.credit
|
||||
FROM accounting_journal j
|
||||
JOIN accounting_entries e ON j.id = e.journal_id
|
||||
WHERE 1=1";
|
||||
$params = [];
|
||||
|
||||
if ($search) {
|
||||
$sql .= " AND (j.description LIKE ? OR j.reference LIKE ? OR e.account_name LIKE ?)";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
}
|
||||
if ($date_from) {
|
||||
$sql .= " AND j.date >= ?";
|
||||
$params[] = $date_from;
|
||||
}
|
||||
if ($date_to) {
|
||||
$sql .= " AND j.date <= ?";
|
||||
$params[] = $date_to;
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY j.date DESC, j.id DESC";
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
function get_ledger_count($search = '', $date_from = '', $date_to = '') {
|
||||
$db = db();
|
||||
$sql = "SELECT COUNT(*)
|
||||
FROM accounting_journal j
|
||||
JOIN accounting_entries e ON j.id = e.journal_id
|
||||
WHERE 1=1";
|
||||
$params = [];
|
||||
|
||||
if ($search) {
|
||||
$sql .= " AND (j.description LIKE ? OR j.reference LIKE ? OR e.account_name LIKE ?)";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
}
|
||||
if ($date_from) {
|
||||
$sql .= " AND j.date >= ?";
|
||||
$params[] = $date_from;
|
||||
}
|
||||
if ($date_to) {
|
||||
$sql .= " AND j.date <= ?";
|
||||
$params[] = $date_to;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
function get_ledger_paginated($search = '', $date_from = '', $date_to = '', $limit = 10, $offset = 0) {
|
||||
$db = db();
|
||||
$sql = "SELECT j.id, j.date, j.description, j.reference, e.account_name, e.debit, e.credit
|
||||
FROM accounting_journal j
|
||||
JOIN accounting_entries e ON j.id = e.journal_id
|
||||
WHERE 1=1";
|
||||
$params = [];
|
||||
|
||||
if ($search) {
|
||||
$sql .= " AND (j.description LIKE ? OR j.reference LIKE ? OR e.account_name LIKE ?)";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
}
|
||||
if ($date_from) {
|
||||
$sql .= " AND j.date >= ?";
|
||||
$params[] = $date_from;
|
||||
}
|
||||
if ($date_to) {
|
||||
$sql .= " AND j.date <= ?";
|
||||
$params[] = $date_to;
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY j.date DESC, j.id DESC LIMIT $limit OFFSET $offset";
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
function get_trial_balance() {
|
||||
$db = db();
|
||||
$stmt = $db->query("SELECT account_name, SUM(debit) as total_debit, SUM(credit) as total_credit
|
||||
FROM accounting_entries
|
||||
GROUP BY account_name");
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
function get_balance_sheet() {
|
||||
$db = db();
|
||||
$stmt = $db->query("SELECT a.name, a.type, SUM(e.debit - e.credit) as balance
|
||||
FROM accounting_accounts a
|
||||
LEFT JOIN accounting_entries e ON a.name = e.account_name
|
||||
GROUP BY a.name, a.type");
|
||||
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$sheet = ['أصول' => 0, 'خصوم' => 0, 'حقوق ملكية' => 0, 'إيرادات' => 0, 'مصروفات' => 0];
|
||||
foreach($data as $row) {
|
||||
if (isset($sheet[$row['type']])) {
|
||||
$sheet[$row['type']] += $row['balance'];
|
||||
}
|
||||
}
|
||||
return $sheet;
|
||||
}
|
||||
|
||||
function get_all_accounts() {
|
||||
return db()->query("SELECT * FROM accounting_accounts ORDER BY type, name")->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
function add_journal_entry($date, $description, $reference, $entries) {
|
||||
$db = db();
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
$stmt = $db->prepare("INSERT INTO accounting_journal (date, description, reference) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$date, $description, $reference]);
|
||||
$journal_id = $db->lastInsertId();
|
||||
|
||||
$stmt = $db->prepare("INSERT INTO accounting_entries (journal_id, account_name, debit, credit) VALUES (?, ?, ?, ?)");
|
||||
foreach ($entries as $entry) {
|
||||
$stmt->execute([$journal_id, $entry['account'], $entry['debit'], $entry['credit']]);
|
||||
}
|
||||
$db->commit();
|
||||
return $journal_id;
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function delete_journal_entry($id) {
|
||||
$db = db();
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
$stmt = $db->prepare("DELETE FROM accounting_entries WHERE journal_id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
$stmt = $db->prepare("DELETE FROM accounting_journal WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
$db->commit();
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function get_journal_entry_details($id) {
|
||||
$db = db();
|
||||
$stmt = $db->prepare("SELECT * FROM accounting_journal WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$journal = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$journal) return null;
|
||||
|
||||
$stmt = $db->prepare("SELECT * FROM accounting_entries WHERE journal_id = ? ORDER BY id ASC");
|
||||
$stmt->execute([$id]);
|
||||
$entries = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$debit_account = '';
|
||||
$credit_account = '';
|
||||
$amount = 0;
|
||||
|
||||
foreach ($entries as $e) {
|
||||
if ($e['debit'] > 0) {
|
||||
$debit_account = $e['account_name'];
|
||||
$amount = $e['debit'];
|
||||
} elseif ($e['credit'] > 0) {
|
||||
$credit_account = $e['account_name'];
|
||||
if ($amount == 0) $amount = $e['credit'];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $journal['id'],
|
||||
'date' => $journal['date'],
|
||||
'description' => $journal['description'],
|
||||
'reference' => $journal['reference'],
|
||||
'debit_account' => $debit_account,
|
||||
'credit_account' => $credit_account,
|
||||
'amount' => $amount
|
||||
];
|
||||
}
|
||||
|
||||
function edit_journal_entry($id, $date, $description, $reference, $entries) {
|
||||
$db = db();
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
$stmt = $db->prepare("UPDATE accounting_journal SET date = ?, description = ?, reference = ? WHERE id = ?");
|
||||
$stmt->execute([$date, $description, $reference, $id]);
|
||||
|
||||
$stmt = $db->prepare("DELETE FROM accounting_entries WHERE journal_id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
$stmt = $db->prepare("INSERT INTO accounting_entries (journal_id, account_name, debit, credit) VALUES (?, ?, ?, ?)");
|
||||
foreach ($entries as $entry) {
|
||||
$stmt->execute([$id, $entry['account'], $entry['debit'], $entry['credit']]);
|
||||
}
|
||||
$db->commit();
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
?>
|
||||
59
includes/footer.php
Normal file
59
includes/footer.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php if (isLoggedIn()): ?>
|
||||
</main><!-- Close main-content -->
|
||||
<?php endif; ?>
|
||||
|
||||
<footer class="footer mt-auto py-4 bg-white border-top">
|
||||
<div class="container-fluid px-md-4 text-center">
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<?php if (!empty($sys_settings['site_footer'])): ?>
|
||||
<div class="mb-3 text-secondary" style="max-width: 800px; line-height: 1.6;">
|
||||
<?= nl2br(htmlspecialchars($sys_settings['site_footer'])) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<span class="text-muted small mb-1">
|
||||
© <?= date('Y') ?> <?= htmlspecialchars($sys_settings['site_name']) ?>. جميع الحقوق محفوظة.
|
||||
</span>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span class="badge bg-secondary opacity-50 fw-normal" style="font-size: 0.65rem;">نسخة النظام 1.3.0</span>
|
||||
<?php if (isAdmin()): ?>
|
||||
<a href="charity-settings.php" onclick="localStorage.setItem('activeSettingsTab', '#general');" class="text-muted text-decoration-none small hover-primary border-start ps-3">
|
||||
<i class="fas fa-cog me-1"></i> الإعدادات
|
||||
</a>
|
||||
<a href="charity-settings.php#logs" onclick="localStorage.setItem('activeSettingsTab', '#logs');" class="text-muted text-decoration-none small hover-primary border-start ps-3">
|
||||
<i class="fas fa-history me-1"></i> سجل المراسلات
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.hover-primary:hover {
|
||||
color: #0d6efd !important;
|
||||
}
|
||||
.footer .border-start {
|
||||
border-color: rgba(0,0,0,0.1) !important;
|
||||
}
|
||||
/* TinyMCE Fixes for Bootstrap Modals */
|
||||
.tox-tinymce-aux {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Bootstrap Bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- TinyMCE Rich Text Editor -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/6.8.2/tinymce.min.js"></script>
|
||||
<!-- SweetAlert2 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<!-- Main App JS -->
|
||||
<script src="assets/js/main.js?v=<?= time() ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
if (ob_get_level() > 0) {
|
||||
ob_end_flush();
|
||||
}
|
||||
?>
|
||||
591
includes/header.php
Normal file
591
includes/header.php
Normal file
@ -0,0 +1,591 @@
|
||||
<?php
|
||||
ob_start(); error_reporting(E_ALL); ini_set("display_errors", 1);
|
||||
session_start();
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
// --- Helper Functions (MUST BE DEFINED BEFORE settings.php) ---
|
||||
require_once __DIR__ . '/permissions.php';
|
||||
|
||||
// Now load centralized settings (which may use the helpers above)
|
||||
require_once __DIR__ . '/settings.php';
|
||||
|
||||
// Fetch user info (theme and permissions)
|
||||
$user_theme = 'light';
|
||||
$current_user = null;
|
||||
if (isLoggedIn()) {
|
||||
$stmt = db()->prepare("SELECT * FROM users WHERE id = ?");
|
||||
$stmt->execute([$_SESSION['user_id']]);
|
||||
$current_user = $stmt->fetch();
|
||||
|
||||
if ($current_user) {
|
||||
$user_theme = $current_user['theme'] ?: 'light';
|
||||
$_SESSION['can_view'] = (bool)$current_user['can_view'];
|
||||
$_SESSION['can_add'] = (bool)$current_user['can_add'];
|
||||
$_SESSION['can_edit'] = (bool)$current_user['can_edit'];
|
||||
$_SESSION['can_delete'] = (bool)$current_user['can_delete'];
|
||||
$_SESSION['name'] = $current_user['full_name'] ?: $current_user['username'];
|
||||
$_SESSION['user_role'] = strtolower($current_user['role']);
|
||||
$_SESSION['role'] = strtolower($current_user['role']);
|
||||
$_SESSION['is_super_admin'] = (int)$current_user['is_super_admin'];
|
||||
|
||||
// Load granular permissions
|
||||
if (!isset($_SESSION['permissions']) || empty($_SESSION['permissions'])) {
|
||||
$perm_stmt = db()->prepare("SELECT * FROM user_permissions WHERE user_id = ?");
|
||||
$perm_stmt->execute([$_SESSION['user_id']]);
|
||||
$perms = $perm_stmt->fetchAll();
|
||||
$_SESSION['permissions'] = [];
|
||||
foreach ($perms as $p) {
|
||||
$_SESSION['permissions'][$p['page']] = [
|
||||
'view' => (bool)$p['can_view'],
|
||||
'add' => (bool)$p['can_add'],
|
||||
'edit' => (bool)$p['can_edit'],
|
||||
'delete' => (bool)$p['can_delete'],
|
||||
];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// User not found in DB but session exists - clean up
|
||||
session_destroy();
|
||||
redirect('login.php');
|
||||
}
|
||||
}
|
||||
|
||||
// Auth Check (after fetch to ensure session is updated)
|
||||
if (!isLoggedIn() && basename($_SERVER['PHP_SELF']) !== 'login.php' && basename($_SERVER['PHP_SELF']) !== 'forgot_password.php' && basename($_SERVER['PHP_SELF']) !== 'install.php') {
|
||||
redirect('login.php');
|
||||
}
|
||||
|
||||
// Determine active groups
|
||||
$cp = basename($_SERVER['PHP_SELF']);
|
||||
|
||||
$mail_pages = ['inbound.php', 'outbound.php', 'internal_inbox.php', 'internal_outbox.php', 'overdue_report.php'];
|
||||
$is_mail_open = in_array($cp, $mail_pages);
|
||||
|
||||
$acct_pages = ['accounting.php', 'trial_balance.php', 'balance_sheet.php', 'accounts.php'];
|
||||
$is_acct_open = in_array($cp, $acct_pages);
|
||||
|
||||
$hr_pages = ['hr_dashboard.php', 'hr_employees.php', 'hr_attendance.php', 'hr_leaves.php', 'hr_holidays.php', 'hr_payroll.php', 'hr_reports.php'];
|
||||
$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);
|
||||
|
||||
$meetings_pages = ['meetings.php'];
|
||||
$is_meetings_open = in_array($cp, $meetings_pages);
|
||||
|
||||
$admin_pages = ['index.php', 'users.php', 'charity-settings.php'];
|
||||
$is_admin_open = in_array($cp, $admin_pages);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl" data-bs-theme="<?= $user_theme ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars($sys_settings['site_name']) ?></title>
|
||||
|
||||
<!-- Bootstrap 5 RTL -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.rtl.min.css">
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
||||
<!-- Google Fonts - Cairo -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
|
||||
|
||||
<?php if ($sys_settings['site_favicon']): ?>
|
||||
<link rel="icon" type="image/x-icon" href="<?= $sys_settings['site_favicon'] ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Cairo', sans-serif;
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
.sidebar {
|
||||
height: 100vh;
|
||||
background-color: #212529;
|
||||
color: #fff;
|
||||
width: 250px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
transition: all 0.3s;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255,255,255,0.2) transparent;
|
||||
}
|
||||
.sidebar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.sidebar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255,255,255,0.2);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.sidebar .nav-link {
|
||||
color: rgba(255,255,255,0.7);
|
||||
padding: 12px 20px;
|
||||
border-radius: 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.sidebar .nav-link:hover, .sidebar .nav-link.active {
|
||||
color: #fff;
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
}
|
||||
.sidebar .nav-link.active {
|
||||
border-right: 4px solid #0d6efd;
|
||||
}
|
||||
.logo-link:hover { opacity: 0.8; }
|
||||
.sidebar-heading {
|
||||
padding: 20px 20px 10px;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1rem;
|
||||
color: rgba(255,255,255,0.4);
|
||||
}
|
||||
.main-content {
|
||||
margin-right: 250px;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.top-navbar {
|
||||
margin-right: 250px;
|
||||
background-color: var(--bs-tertiary-bg);
|
||||
border-bottom: 1px solid var(--bs-border-color);
|
||||
}
|
||||
@media (max-width: 991.98px) {
|
||||
.sidebar {
|
||||
right: -250px;
|
||||
}
|
||||
.sidebar.show {
|
||||
right: 0;
|
||||
}
|
||||
.main-content, .top-navbar {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* RTL Fixes for Bootstrap */
|
||||
.ms-auto { margin-right: auto !important; margin-left: 0 !important; }
|
||||
.me-auto { margin-left: auto !important; margin-right: 0 !important; }
|
||||
|
||||
/* RTL specific tweaks */
|
||||
[dir="rtl"] .dropdown-menu { text-align: right; }
|
||||
[dir="rtl"] .ms-2 { margin-right: 0.5rem !important; margin-left: 0 !important; }
|
||||
[dir="rtl"] .me-2 { margin-left: 0.5rem !important; margin-right: 0 !important; }
|
||||
[dir="rtl"] .me-1 { margin-left: 0.25rem !important; margin-right: 0 !important; }
|
||||
</style>
|
||||
<script>
|
||||
function toggleSidebar() {
|
||||
document.getElementById('sidebar').classList.toggle('show');
|
||||
}
|
||||
function setTheme(theme) {
|
||||
document.documentElement.setAttribute('data-bs-theme', theme);
|
||||
fetch('api/update_theme.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ theme: theme })
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<?php if (isLoggedIn()): ?>
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar d-flex flex-column" id="sidebar">
|
||||
<div class="p-3 text-center border-bottom border-secondary">
|
||||
<a href="user_dashboard.php" class="text-decoration-none text-white d-block logo-link">
|
||||
<?php if (!empty($sys_settings['site_logo'])): ?>
|
||||
<img src="<?= $sys_settings['site_logo'] ?>" alt="Logo" class="img-fluid mb-2" style="max-height: 50px;">
|
||||
<?php endif; ?>
|
||||
<h5 class="mb-0 fw-bold"><?= htmlspecialchars($sys_settings['site_name']) ?></h5>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ul class="nav flex-column mt-3 mb-4">
|
||||
<!-- Dashboard -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= ($cp == 'user_dashboard.php') ? 'active' : '' ?>" href="user_dashboard.php">
|
||||
<i class="fas fa-tachometer-alt me-2"></i> لوحة التحكم
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Mail Group -->
|
||||
<?php if (canView('inbound') || canView('outbound') || canView('internal') || canView('reports')): ?>
|
||||
<li class="nav-item">
|
||||
<button class="sidebar-group-btn <?= $is_mail_open ? '' : 'collapsed' ?>" type="button" data-bs-toggle="collapse" data-bs-target="#menu-mail" aria-expanded="<?= $is_mail_open ? 'true' : 'false' ?>">
|
||||
<span class="group-content group-mail">
|
||||
<i class="fas fa-envelope"></i> البريد
|
||||
</span>
|
||||
<i class="fas fa-chevron-down arrow-icon"></i>
|
||||
</button>
|
||||
<div class="collapse <?= $is_mail_open ? 'show' : '' ?>" id="menu-mail">
|
||||
<ul class="nav flex-column">
|
||||
<?php if (canView('inbound')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= ($cp == 'inbound.php' && !isset($_GET['my_tasks'])) ? 'active' : '' ?>" href="inbound.php">
|
||||
البريد الوارد
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (canView('outbound')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'outbound.php' ? 'active' : '' ?>" href="outbound.php">
|
||||
البريد الصادر
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (canView('internal')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'internal_inbox.php' ? 'active' : '' ?>" href="internal_inbox.php">
|
||||
الوارد الداخلي
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'internal_outbox.php' ? 'active' : '' ?>" href="internal_outbox.php">
|
||||
الصادر الداخلي
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (canView('reports')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'overdue_report.php' ? 'active' : '' ?>" href="overdue_report.php">
|
||||
تقرير التأخير
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Accounting Group -->
|
||||
<?php if (canView('accounting')): ?>
|
||||
<li class="nav-item">
|
||||
<button class="sidebar-group-btn <?= $is_acct_open ? '' : 'collapsed' ?>" type="button" data-bs-toggle="collapse" data-bs-target="#menu-acct" aria-expanded="<?= $is_acct_open ? 'true' : 'false' ?>">
|
||||
<span class="group-content group-acct">
|
||||
<i class="fas fa-calculator"></i> المحاسبة
|
||||
</span>
|
||||
<i class="fas fa-chevron-down arrow-icon"></i>
|
||||
</button>
|
||||
<div class="collapse <?= $is_acct_open ? 'show' : '' ?>" id="menu-acct">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'accounting.php' ? 'active' : '' ?>" href="accounting.php">
|
||||
المحاسبة العامة
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'trial_balance.php' ? 'active' : '' ?>" href="trial_balance.php">
|
||||
ميزان المراجعة
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'balance_sheet.php' ? 'active' : '' ?>" href="balance_sheet.php">
|
||||
الميزانية العمومية
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'accounts.php' ? 'active' : '' ?>" href="accounts.php">
|
||||
دليل الحسابات
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- HR Group -->
|
||||
<?php if (canView('hr_employees') || canView('hr_dashboard')): ?>
|
||||
<li class="nav-item">
|
||||
<button class="sidebar-group-btn <?= $is_hr_open ? '' : 'collapsed' ?>" type="button" data-bs-toggle="collapse" data-bs-target="#menu-hr" aria-expanded="<?= $is_hr_open ? 'true' : 'false' ?>">
|
||||
<span class="group-content group-hr">
|
||||
<i class="fas fa-users-cog"></i> الموارد البشرية
|
||||
</span>
|
||||
<i class="fas fa-chevron-down arrow-icon"></i>
|
||||
</button>
|
||||
<div class="collapse <?= $is_hr_open ? 'show' : '' ?>" id="menu-hr">
|
||||
<ul class="nav flex-column">
|
||||
<?php if (canView('hr_dashboard')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'hr_dashboard.php' ? 'active' : '' ?>" href="hr_dashboard.php">
|
||||
لوحة HR
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if (canView('hr_employees')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'hr_employees.php' ? 'active' : '' ?>" href="hr_employees.php">
|
||||
الموظفين
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if (canView('hr_attendance')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'hr_attendance.php' ? 'active' : '' ?>" href="hr_attendance.php">
|
||||
الحضور والانصراف
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if (canView('hr_leaves')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'hr_leaves.php' ? 'active' : '' ?>" href="hr_leaves.php">
|
||||
الإجازات
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if (canView('hr_attendance')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'hr_holidays.php' ? 'active' : '' ?>" href="hr_holidays.php">
|
||||
العطلات
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if (canView('hr_payroll')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'hr_payroll.php' ? 'active' : '' ?>" href="hr_payroll.php">
|
||||
الرواتب
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if (canView('hr_reports')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'hr_reports.php' ? 'active' : '' ?>" href="hr_reports.php">
|
||||
التقارير
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Stock Group -->
|
||||
<?php if (canView('stock_dashboard') || canView('stock_items') || canView('stock_settings')): ?>
|
||||
<li class="nav-item">
|
||||
<button class="sidebar-group-btn <?= $is_stock_open ? '' : 'collapsed' ?>" type="button" data-bs-toggle="collapse" data-bs-target="#menu-stock" aria-expanded="<?= $is_stock_open ? 'true' : 'false' ?>">
|
||||
<span class="group-content group-stock">
|
||||
<i class="fas fa-boxes"></i> إدارة المخزون
|
||||
</span>
|
||||
<i class="fas fa-chevron-down arrow-icon"></i>
|
||||
</button>
|
||||
<div class="collapse <?= $is_stock_open ? 'show' : '' ?>" id="menu-stock">
|
||||
<ul class="nav flex-column">
|
||||
<?php if (canView('stock_dashboard')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'stock_dashboard.php' ? 'active' : '' ?>" href="stock_dashboard.php">
|
||||
لوحة التحكم
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if (canView('stock_items')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'stock_items.php' ? 'active' : '' ?>" href="stock_items.php">
|
||||
الأصناف والمخزون
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if (canView('stock_in')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'stock_in.php' ? 'active' : '' ?>" href="stock_in.php">
|
||||
توريد (وارد)
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if (canView('stock_out')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'stock_out.php' ? 'active' : '' ?>" href="stock_out.php">
|
||||
صرف (صادر)
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if (canView('stock_lending')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'stock_lending.php' ? 'active' : '' ?>" href="stock_lending.php">
|
||||
الإعارة
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if (canView('stock_reports')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'stock_reports.php' ? 'active' : '' ?>" href="stock_reports.php">
|
||||
التقارير
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if (canView('stock_settings')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'stock_settings.php' ? 'active' : '' ?>" href="stock_settings.php">
|
||||
إعدادات المخزون
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</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; ?>
|
||||
|
||||
<!-- Meetings Group -->
|
||||
<?php if (canView('meetings')): ?>
|
||||
<li class="nav-item">
|
||||
<button class="sidebar-group-btn <?= $is_meetings_open ? '' : 'collapsed' ?>" type="button" data-bs-toggle="collapse" data-bs-target="#menu-meetings" aria-expanded="<?= $is_meetings_open ? 'true' : 'false' ?>">
|
||||
<span class="group-content group-meetings">
|
||||
<i class="fas fa-handshake"></i> الاجتماعات
|
||||
</span>
|
||||
<i class="fas fa-chevron-down arrow-icon"></i>
|
||||
</button>
|
||||
<div class="collapse <?= $is_meetings_open ? 'show' : '' ?>" id="menu-meetings">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'meetings.php' ? 'active' : '' ?>" href="meetings.php">
|
||||
جدول الاجتماعات
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Admin Group -->
|
||||
<?php if (canView('users') || canView('settings') || isAdmin()): ?>
|
||||
<li class="nav-item">
|
||||
<button class="sidebar-group-btn <?= $is_admin_open ? '' : 'collapsed' ?>" type="button" data-bs-toggle="collapse" data-bs-target="#menu-admin" aria-expanded="<?= $is_admin_open ? 'true' : 'false' ?>">
|
||||
<span class="group-content group-admin">
|
||||
<i class="fas fa-cogs"></i> الإدارة
|
||||
</span>
|
||||
<i class="fas fa-chevron-down arrow-icon"></i>
|
||||
</button>
|
||||
<div class="collapse <?= $is_admin_open ? 'show' : '' ?>" id="menu-admin">
|
||||
<ul class="nav flex-column">
|
||||
<?php if (isAdmin()): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'index.php' ? 'active' : '' ?>" href="index.php">
|
||||
إحصائيات النظام
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (canView('users')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'users.php' ? 'active' : '' ?>" href="users.php">
|
||||
إدارة المستخدمين
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (canView('settings')): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $cp == 'charity-settings.php' ? 'active' : '' ?>" href="charity-settings.php">
|
||||
إعدادات النظام
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
|
||||
<div class="mt-auto p-3 text-center opacity-50 small">
|
||||
© <?= date('Y') ?> <?= htmlspecialchars($sys_settings['site_name']) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Navbar -->
|
||||
<nav class="navbar navbar-expand-lg top-navbar sticky-top p-0 shadow-sm">
|
||||
<div class="container-fluid px-3">
|
||||
<button class="btn d-lg-none" type="button" onclick="toggleSidebar()">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
|
||||
<div class="ms-auto d-flex align-items-center">
|
||||
|
||||
<!-- Theme Dropdown -->
|
||||
<div class="dropdown me-3">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle d-flex align-items-center gap-2" type="button" id="themeDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fas fa-palette"></i>
|
||||
<span class="d-none d-md-inline">المظهر</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow border-0" aria-labelledby="themeDropdown">
|
||||
<li><button class="dropdown-item d-flex align-items-center" onclick="setTheme('light')"><i class="fas fa-sun me-2 text-warning"></i> فاتح (Light)</button></li>
|
||||
<li><button class="dropdown-item d-flex align-items-center" onclick="setTheme('dark')"><i class="fas fa-moon me-2 text-dark"></i> داكن (Dark)</button></li>
|
||||
<li><button class="dropdown-item d-flex align-items-center" onclick="setTheme('midnight')"><i class="fas fa-star me-2 text-primary"></i> منتصف الليل (Midnight)</button></li>
|
||||
<li><button class="dropdown-item d-flex align-items-center" onclick="setTheme('forest')"><i class="fas fa-tree me-2 text-success"></i> غابة (Forest)</button></li>
|
||||
<li><button class="dropdown-item d-flex align-items-center" onclick="setTheme('ocean')"><i class="fas fa-water me-2 text-info"></i> محيط (Ocean)</button></li>
|
||||
<li><button class="dropdown-item d-flex align-items-center" onclick="setTheme('sunset')"><i class="fas fa-cloud-sun me-2 text-danger"></i> غروب (Sunset)</button></li>
|
||||
<li><button class="dropdown-item d-flex align-items-center" onclick="setTheme('royal')"><i class="fas fa-crown me-2 text-secondary"></i> ملكي (Royal)</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn d-flex align-items-center dropdown-toggle border-0" type="button" id="userMenu" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<div class="text-end me-2 d-none d-md-block">
|
||||
<div class="fw-bold small"><?= htmlspecialchars($_SESSION['name'] ?? 'المستخدم') ?></div>
|
||||
<div class="text-muted" style="font-size: 0.7rem;"><?= ucfirst($_SESSION['user_role'] ?? 'موظف') ?></div>
|
||||
</div>
|
||||
<?php if (!empty($current_user['profile_image'])): ?>
|
||||
<img src="<?= $current_user['profile_image'] ?>" alt="Profile" class="rounded-circle" width="35" height="35" style="object-fit: cover;">
|
||||
<?php else: ?>
|
||||
<div class="rounded-circle bg-primary bg-opacity-10 d-flex align-items-center justify-content-center" style="width: 35px; height: 35px;">
|
||||
<i class="fas fa-user text-primary"></i>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow border-0" aria-labelledby="userMenu">
|
||||
<li><a class="dropdown-item" href="profile.php"><i class="fas fa-user-circle me-2 text-muted"></i> ملفي الشخصي</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item text-danger" href="logout.php"><i class="fas fa-sign-out-alt me-2"></i> تسجيل الخروج</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<?php endif; ?>
|
||||
100
includes/pagination.php
Normal file
100
includes/pagination.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Calculate pagination parameters.
|
||||
*
|
||||
* @param int $page Current page number (1-based)
|
||||
* @param int $total Total number of items
|
||||
* @param int $perPage Items per page (default 10)
|
||||
* @return array ['offset' => int, 'limit' => int, 'total_pages' => int, 'current_page' => int]
|
||||
*/
|
||||
function getPagination($page, $total, $perPage = 10) {
|
||||
$page = max(1, (int)$page);
|
||||
$perPage = max(1, (int)$perPage);
|
||||
$totalPages = ceil($total / $perPage);
|
||||
|
||||
// Ensure page doesn't exceed total pages (unless total is 0)
|
||||
if ($totalPages > 0 && $page > $totalPages) {
|
||||
$page = $totalPages;
|
||||
}
|
||||
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
return [
|
||||
'offset' => $offset,
|
||||
'limit' => $perPage,
|
||||
'total_pages' => $totalPages,
|
||||
'current_page' => $page
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to build pagination link with existing query params
|
||||
*/
|
||||
function getPaginationLink($page, $queryParams = []) {
|
||||
$params = array_merge($_GET, $queryParams);
|
||||
$params['page'] = $page;
|
||||
return '?' . http_build_query($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render pagination links using Bootstrap 5.
|
||||
*
|
||||
* @param int $currentPage
|
||||
* @param int $totalPages
|
||||
* @return string HTML for pagination
|
||||
*/
|
||||
function renderPagination($currentPage, $totalPages) {
|
||||
if ($totalPages <= 1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$html = '<nav aria-label="Page navigation" class="mt-4"><ul class="pagination justify-content-center">';
|
||||
|
||||
// Previous Button
|
||||
if ($currentPage > 1) {
|
||||
$html .= '<li class="page-item"><a class="page-link" href="' . getPaginationLink($currentPage - 1) . '">السابق</a></li>';
|
||||
} else {
|
||||
$html .= '<li class="page-item disabled"><span class="page-link">السابق</span></li>';
|
||||
}
|
||||
|
||||
// Page Numbers
|
||||
// Simple logic: Show all if <= 7, else show start, end, and around current
|
||||
// For simplicity in this iteration, let's show a sliding window or just simple list
|
||||
// Let's do a simple sliding window of 5
|
||||
$start = max(1, $currentPage - 2);
|
||||
$end = min($totalPages, $currentPage + 2);
|
||||
|
||||
if ($start > 1) {
|
||||
$html .= '<li class="page-item"><a class="page-link" href="' . getPaginationLink(1) . '">1</a></li>';
|
||||
if ($start > 2) {
|
||||
$html .= '<li class="page-item disabled"><span class="page-link">...</span></li>';
|
||||
}
|
||||
}
|
||||
|
||||
for ($i = $start; $i <= $end; $i++) {
|
||||
$active = ($i == $currentPage) ? 'active' : '';
|
||||
if ($i == $currentPage) {
|
||||
$html .= '<li class="page-item active"><span class="page-link">' . $i . '</span></li>';
|
||||
} else {
|
||||
$html .= '<li class="page-item"><a class="page-link" href="' . getPaginationLink($i) . '">' . $i . '</a></li>';
|
||||
}
|
||||
}
|
||||
|
||||
if ($end < $totalPages) {
|
||||
if ($end < $totalPages - 1) {
|
||||
$html .= '<li class="page-item disabled"><span class="page-link">...</span></li>';
|
||||
}
|
||||
$html .= '<li class="page-item"><a class="page-link" href="' . getPaginationLink($totalPages) . '">' . $totalPages . '</a></li>';
|
||||
}
|
||||
|
||||
// Next Button
|
||||
if ($currentPage < $totalPages) {
|
||||
$html .= '<li class="page-item"><a class="page-link" href="' . getPaginationLink($currentPage + 1) . '">التالي</a></li>';
|
||||
} else {
|
||||
$html .= '<li class="page-item disabled"><span class="page-link">التالي</span></li>';
|
||||
}
|
||||
|
||||
$html .= '</ul></nav>';
|
||||
return $html;
|
||||
}
|
||||
64
includes/permissions.php
Normal file
64
includes/permissions.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
// --- Helper Functions (Extracted from header.php) ---
|
||||
|
||||
function isLoggedIn() {
|
||||
return isset($_SESSION['user_id']);
|
||||
}
|
||||
|
||||
function isSuperAdmin() {
|
||||
return isset($_SESSION['is_super_admin']) && $_SESSION['is_super_admin'] == 1;
|
||||
}
|
||||
|
||||
function isAdmin() {
|
||||
if (isSuperAdmin()) return true;
|
||||
if (isset($_SESSION['user_role']) && strtolower($_SESSION['user_role']) === 'admin') return true;
|
||||
if (isset($_SESSION['role']) && strtolower($_SESSION['role']) === 'admin') return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function redirect($path) {
|
||||
if (!headers_sent()) {
|
||||
header("Location: $path");
|
||||
} else {
|
||||
echo "<script>window.location.href='$path';</script>";
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// Permission helpers
|
||||
function canView($page = null) {
|
||||
if (isAdmin()) return true;
|
||||
if ($page) {
|
||||
return $_SESSION['permissions'][$page]['view'] ?? false;
|
||||
}
|
||||
return $_SESSION['can_view'] ?? false;
|
||||
}
|
||||
|
||||
function canAdd($page = null) {
|
||||
if (isAdmin()) return true;
|
||||
if ($page) {
|
||||
return $_SESSION['permissions'][$page]['add'] ?? false;
|
||||
}
|
||||
return $_SESSION['can_add'] ?? false;
|
||||
}
|
||||
|
||||
function canEdit($page = null) {
|
||||
if (isAdmin()) return true;
|
||||
if ($page) {
|
||||
return $_SESSION['permissions'][$page]['edit'] ?? false;
|
||||
}
|
||||
return $_SESSION['can_edit'] ?? false;
|
||||
}
|
||||
|
||||
function canDelete($page = null) {
|
||||
if (isAdmin()) return true;
|
||||
if ($page) {
|
||||
return $_SESSION['permissions'][$page]['delete'] ?? false;
|
||||
}
|
||||
return $_SESSION['can_delete'] ?? false;
|
||||
}
|
||||
|
||||
function canViewInternal() {
|
||||
return canView('internal');
|
||||
}
|
||||
98
includes/settings.php
Normal file
98
includes/settings.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/**
|
||||
* Central Settings Management
|
||||
* Loads all system settings from database tables (charity_settings, smtp_settings)
|
||||
*/
|
||||
|
||||
if (!function_exists('get_settings')) {
|
||||
function get_settings() {
|
||||
static $settings = null;
|
||||
if ($settings === null) {
|
||||
try {
|
||||
// Fetch Charity Info
|
||||
$charity_stmt = db()->query("SELECT * FROM charity_settings WHERE id = 1");
|
||||
$charity = $charity_stmt->fetch();
|
||||
|
||||
// Fetch SMTP Info
|
||||
$smtp_stmt = db()->query("SELECT * FROM smtp_settings WHERE id = 1");
|
||||
$smtp = $smtp_stmt->fetch();
|
||||
|
||||
$settings = [
|
||||
'site_name' => $charity['charity_name'] ?? 'نظام إدارة البريد',
|
||||
'site_slogan' => $charity['charity_slogan'] ?? '',
|
||||
'site_email' => $charity['charity_email'] ?? '',
|
||||
'site_phone' => $charity['charity_phone'] ?? '',
|
||||
'site_address' => $charity['charity_address'] ?? '',
|
||||
'site_logo' => $charity['charity_logo'] ?? '',
|
||||
'site_favicon' => $charity['charity_favicon'] ?? '',
|
||||
'site_maintenance' => (bool)($charity['site_maintenance'] ?? 0),
|
||||
'site_footer' => $charity['site_footer'] ?? '',
|
||||
'allow_registration' => (bool)($charity['allow_registration'] ?? 0),
|
||||
|
||||
'smtp' => [
|
||||
'host' => $smtp['smtp_host'] ?? '',
|
||||
'port' => $smtp['smtp_port'] ?? 587,
|
||||
'secure' => $smtp['smtp_secure'] ?? 'tls',
|
||||
'user' => $smtp['smtp_user'] ?? '',
|
||||
'pass' => $smtp['smtp_pass'] ?? '',
|
||||
'from_email' => $smtp['from_email'] ?? '',
|
||||
'from_name' => $smtp['from_name'] ?? '',
|
||||
'reply_to' => $smtp['reply_to'] ?? '',
|
||||
'enabled' => (bool)($smtp['is_enabled'] ?? 1),
|
||||
'failures' => (int)($smtp['consecutive_failures'] ?? 0),
|
||||
'max_failures' => (int)($smtp['max_failures'] ?? 5)
|
||||
]
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
// Fallback settings if DB is not ready
|
||||
$settings = [
|
||||
'site_name' => 'نظام إدارة البريد',
|
||||
'site_slogan' => '',
|
||||
'site_maintenance' => false,
|
||||
'smtp' => ['enabled' => false]
|
||||
];
|
||||
}
|
||||
}
|
||||
return $settings;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compatibility wrapper for getCharitySettings
|
||||
* @return array
|
||||
*/
|
||||
if (!function_exists('getCharitySettings')) {
|
||||
function getCharitySettings() {
|
||||
$settings = get_settings();
|
||||
// Return only the keys that might be expected by older code
|
||||
return [
|
||||
'charity_name' => $settings['site_name'],
|
||||
'charity_slogan' => $settings['site_slogan'],
|
||||
'charity_email' => $settings['site_email'],
|
||||
'charity_phone' => $settings['site_phone'],
|
||||
'charity_address' => $settings['site_address'],
|
||||
'charity_logo' => $settings['site_logo'],
|
||||
'charity_favicon' => $settings['site_favicon'],
|
||||
'site_maintenance' => $settings['site_maintenance'],
|
||||
'site_footer' => $settings['site_footer'],
|
||||
'allow_registration' => $settings['allow_registration']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Global settings variable
|
||||
$sys_settings = get_settings();
|
||||
|
||||
// Maintenance Mode Check - only if not admin and not on login/logout
|
||||
// Assuming isAdmin() exists, if not we should be careful
|
||||
if ($sys_settings['site_maintenance'] && basename($_SERVER['PHP_SELF']) !== 'login.php' && basename($_SERVER['PHP_SELF']) !== 'logout.php') {
|
||||
// If isAdmin is not defined, we might need a fallback or just check session
|
||||
$is_admin = false;
|
||||
if (isset($_SESSION['user_role']) && $_SESSION['user_role'] === 'admin') {
|
||||
$is_admin = true;
|
||||
}
|
||||
|
||||
if (!$is_admin) {
|
||||
die("<h1>النظام تحت الصيانة حالياً</h1><p>يرجى المحاولة مرة أخرى في وقت لاحق.</p>");
|
||||
}
|
||||
}
|
||||
469
index.php
469
index.php
@ -1,150 +1,333 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
// Role-based routing: Admins stay here, others go to their dashboard
|
||||
if (!isAdmin()) {
|
||||
redirect('user_dashboard.php');
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$is_admin = isAdmin();
|
||||
|
||||
// Stats - Total counts from separate tables
|
||||
$total_inbound = canView('inbound') ? db()->query("SELECT COUNT(*) FROM inbound_mail")->fetchColumn() : 0;
|
||||
$total_outbound = canView('outbound') ? db()->query("SELECT COUNT(*) FROM outbound_mail")->fetchColumn() : 0;
|
||||
|
||||
// Fetch statuses for badge and count
|
||||
$statuses_data = db()->query("SELECT * FROM mailbox_statuses")->fetchAll(PDO::FETCH_UNIQUE);
|
||||
|
||||
$in_progress_id = null;
|
||||
foreach ($statuses_data as $id => $s) {
|
||||
if ($s['name'] == 'in_progress') {
|
||||
$in_progress_id = $id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$in_progress_count = 0;
|
||||
if ($in_progress_id) {
|
||||
if (canView('inbound')) {
|
||||
$stmt = db()->prepare("SELECT COUNT(*) FROM inbound_mail WHERE status_id = ?");
|
||||
$stmt->execute([$in_progress_id]);
|
||||
$in_progress_count += $stmt->fetchColumn();
|
||||
}
|
||||
if (canView('outbound')) {
|
||||
$stmt = db()->prepare("SELECT COUNT(*) FROM outbound_mail WHERE status_id = ?");
|
||||
$stmt->execute([$in_progress_id]);
|
||||
$in_progress_count += $stmt->fetchColumn();
|
||||
}
|
||||
}
|
||||
|
||||
// My Assignments - Combine from all tables
|
||||
$my_assignments = [];
|
||||
$queries = [];
|
||||
if (canView('inbound')) {
|
||||
$queries[] = "SELECT id, 'inbound' as type, ref_no, subject, due_date, status_id, created_at FROM inbound_mail WHERE assigned_to = $user_id";
|
||||
}
|
||||
if (canView('outbound')) {
|
||||
$queries[] = "SELECT id, 'outbound' as type, ref_no, subject, due_date, status_id, created_at FROM outbound_mail WHERE assigned_to = $user_id";
|
||||
}
|
||||
if (canView('internal')) {
|
||||
$queries[] = "SELECT id, 'internal' as type, ref_no, subject, due_date, status_id, created_at FROM internal_mail WHERE assigned_to = $user_id";
|
||||
}
|
||||
|
||||
if (!empty($queries)) {
|
||||
$full_query = "(" . implode(") UNION ALL (", $queries) . ") ORDER BY created_at DESC LIMIT 5";
|
||||
$stmt = db()->query($full_query);
|
||||
$my_assignments = $stmt->fetchAll();
|
||||
|
||||
// Add status info to assignments
|
||||
foreach ($my_assignments as &$m) {
|
||||
$m['status_name'] = $statuses_data[$m['status_id']]['name'] ?? 'unknown';
|
||||
$m['status_color'] = $statuses_data[$m['status_id']]['color'] ?? '#6c757d';
|
||||
}
|
||||
}
|
||||
|
||||
// Recent Mail (Global for Admin/Clerk, otherwise limited)
|
||||
$recent_mail = [];
|
||||
$recent_queries = [];
|
||||
if (canView('inbound')) {
|
||||
$recent_queries[] = "SELECT m.id, 'inbound' as type, m.ref_no, m.subject, m.due_date, m.sender, m.recipient, m.status_id, m.assigned_to, m.created_by, m.date_registered, m.created_at, u.full_name as assigned_to_name
|
||||
FROM inbound_mail m LEFT JOIN users u ON m.assigned_to = u.id";
|
||||
}
|
||||
if (canView('outbound')) {
|
||||
$recent_queries[] = "SELECT m.id, 'outbound' as type, m.ref_no, m.subject, m.due_date, m.sender, m.recipient, m.status_id, m.assigned_to, m.created_by, m.date_registered, m.created_at, u.full_name as assigned_to_name
|
||||
FROM outbound_mail m LEFT JOIN users u ON m.assigned_to = u.id";
|
||||
}
|
||||
|
||||
if (!empty($recent_queries)) {
|
||||
$full_recent_query = "(" . implode(") UNION ALL (", $recent_queries) . ")";
|
||||
|
||||
if (!$is_admin && ($_SESSION['user_role'] ?? '') !== 'clerk') {
|
||||
$full_recent_query = "SELECT * FROM ($full_recent_query) AS combined WHERE assigned_to = $user_id OR created_by = $user_id ORDER BY created_at DESC LIMIT 10";
|
||||
} else {
|
||||
$full_recent_query = "SELECT * FROM ($full_recent_query) AS combined ORDER BY created_at DESC LIMIT 10";
|
||||
}
|
||||
|
||||
$stmt = db()->query($full_recent_query);
|
||||
$recent_mail = $stmt->fetchAll();
|
||||
|
||||
// Add status info
|
||||
foreach ($recent_mail as &$m) {
|
||||
$m['status_name'] = $statuses_data[$m['status_id']]['name'] ?? 'unknown';
|
||||
$m['status_color'] = $statuses_data[$m['status_id']]['color'] ?? '#6c757d';
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusBadge($mail) {
|
||||
$status_name = $mail['status_name'] ?? 'غير معروف';
|
||||
$status_color = $mail['status_color'] ?? '#6c757d';
|
||||
|
||||
$display_name = $status_name;
|
||||
if ($status_name == 'received') $display_name = 'تم الاستلام';
|
||||
if ($status_name == 'in_progress') $display_name = 'قيد المعالجة';
|
||||
if ($status_name == 'closed') $display_name = 'مكتمل';
|
||||
|
||||
return '<span class="badge" style="background-color: ' . $status_color . ';">' . htmlspecialchars($display_name) . '</span>';
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
|
||||
<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">
|
||||
<div class="btn-group me-2">
|
||||
<?php if (canView('settings')): ?>
|
||||
<a href="charity-settings.php" class="btn btn-sm btn-outline-dark"><i class="fas fa-cog me-1"></i> الإعدادات</a>
|
||||
<?php endif; ?>
|
||||
<?php if (canAdd('inbound')): ?>
|
||||
<a href="inbound.php?action=add" class="btn btn-sm btn-outline-primary">إضافة بريد وارد</a>
|
||||
<?php endif; ?>
|
||||
<?php if (canAdd('outbound')): ?>
|
||||
<a href="outbound.php?action=add" class="btn btn-sm btn-outline-secondary">إضافة بريد صادر</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overdue Alert -->
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
if (canView('reports')):
|
||||
// Combine overdue counts from inbound and outbound
|
||||
$overdue_count = 0;
|
||||
$overdue_count += db()->query("SELECT COUNT(*) FROM inbound_mail WHERE due_date < CURDATE() AND status_id IN (SELECT id FROM mailbox_statuses WHERE name != 'closed')")->fetchColumn();
|
||||
$overdue_count += db()->query("SELECT COUNT(*) FROM outbound_mail WHERE due_date < CURDATE() AND status_id IN (SELECT id FROM mailbox_statuses WHERE name != 'closed')")->fetchColumn();
|
||||
|
||||
if ($overdue_count > 0):
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.loader {
|
||||
margin: 1.25rem auto 1.25rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
code {
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-danger shadow-sm border-0 d-flex align-items-center justify-content-between mb-0">
|
||||
<div>
|
||||
<i class="fas fa-exclamation-triangle fs-4 me-3"></i>
|
||||
<span class="fw-bold">هناك <?= $overdue_count ?> مهام متأخرة تتطلب انتباهك!</span>
|
||||
</div>
|
||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||
<a href="overdue_report.php" class="btn btn-danger btn-sm">عرض التقرير</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
endif;
|
||||
endif;
|
||||
?>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="row g-4 mb-4">
|
||||
<?php if (canView('inbound')): ?>
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 p-3 shadow-sm border-0 border-start border-primary border-4">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h6 class="text-muted mb-1">البريد الوارد</h6>
|
||||
<h3 class="fw-bold mb-0"><?= $total_inbound ?></h3>
|
||||
</div>
|
||||
<div class="bg-primary bg-opacity-10 p-3 rounded-circle">
|
||||
<i class="fas fa-download text-primary fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (canView('outbound')): ?>
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 p-3 shadow-sm border-0 border-start border-success border-4">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h6 class="text-muted mb-1">البريد الصادر</h6>
|
||||
<h3 class="fw-bold mb-0"><?= $total_outbound ?></h3>
|
||||
</div>
|
||||
<div class="bg-success bg-opacity-10 p-3 rounded-circle">
|
||||
<i class="fas fa-upload text-success fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (canView('inbound') || canView('outbound')): ?>
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 p-3 shadow-sm border-0 border-start border-info border-4">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h6 class="text-muted mb-1">قيد المعالجة</h6>
|
||||
<h3 class="fw-bold mb-0"><?= $in_progress_count ?></h3>
|
||||
</div>
|
||||
<div class="bg-info bg-opacity-10 p-3 rounded-circle">
|
||||
<i class="fas fa-clock text-info fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (canView('users')): ?>
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 p-3 shadow-sm border-0 border-start border-warning border-4">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h6 class="text-muted mb-1">المستخدمين</h6>
|
||||
<h3 class="fw-bold mb-0"><?= db()->query("SELECT COUNT(*) FROM users")->fetchColumn() ?></h3>
|
||||
</div>
|
||||
<div class="bg-warning bg-opacity-10 p-3 rounded-circle">
|
||||
<i class="fas fa-users text-warning fs-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($my_assignments)): ?>
|
||||
<!-- My Assignments Section -->
|
||||
<div class="card shadow-sm border-0 mb-4 bg-primary bg-opacity-10 border-top border-primary border-3">
|
||||
<div class="card-header bg-transparent py-3 border-0">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 fw-bold text-primary"><i class="fas fa-tasks me-2"></i> مهامي الحالية</h5>
|
||||
<span class="badge bg-primary rounded-pill"><?= count($my_assignments) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<tbody>
|
||||
<?php foreach ($my_assignments as $mail): ?>
|
||||
<tr style="cursor: pointer;" onclick="window.location='view_mail.php?id=<?= $mail['id'] ?>&type=<?= $mail['type'] ?>'">
|
||||
<td class="ps-4" width="120">
|
||||
<small class="text-muted d-block">رقم القيد</small>
|
||||
<span class="fw-bold text-primary"><?= $mail['ref_no'] ?></span>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted d-block">الموضوع</small>
|
||||
<span class="fw-bold"><?= htmlspecialchars($mail['subject']) ?></span>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted d-block">الموعد النهائي</small>
|
||||
<?php if ($mail['due_date']): ?>
|
||||
<span class="<?= (strtotime($mail['due_date']) < time() && $mail['status_name'] != 'closed') ? 'text-danger fw-bold' : '' ?>">
|
||||
<?= $mail['due_date'] ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">-</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<small class="text-muted d-block mb-1">الحالة</small>
|
||||
<?= getStatusBadge($mail) ?>
|
||||
</td>
|
||||
<td class="pe-4 text-end">
|
||||
<i class="fas fa-chevron-left text-primary"></i>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($recent_mail)): ?>
|
||||
<!-- Recent Mail -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 fw-bold"><?= $is_admin ? 'آخر المراسلات المسجلة' : 'آخر المراسلات' ?></h5>
|
||||
<a href="inbound.php" class="btn btn-sm btn-link text-decoration-none">عرض الكل</a>
|
||||
</div>
|
||||
</div>
|
||||
<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="pe-4 text-center">التاريخ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($recent_mail as $mail): ?>
|
||||
<tr style="cursor: pointer;" onclick="window.location='view_mail.php?id=<?= $mail['id'] ?>&type=<?= $mail['type'] ?>'">
|
||||
<td class="ps-4 fw-bold text-primary"><?= $mail['ref_no'] ?></td>
|
||||
<td>
|
||||
<?php if ($mail['type'] == 'inbound'): ?>
|
||||
<span class="text-primary"><i class="fas fa-arrow-down me-1"></i> وارد</span>
|
||||
<?php else: ?>
|
||||
<span class="text-success"><i class="fas fa-arrow-up me-1"></i> صادر</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($mail['subject']) ?></td>
|
||||
<td>
|
||||
<?php if ($mail['due_date']): ?>
|
||||
<small class="<?= (strtotime($mail['due_date']) < time() && $mail['status_name'] != 'closed') ? 'text-danger fw-bold' : 'text-muted' ?>">
|
||||
<?= $mail['due_date'] ?>
|
||||
</small>
|
||||
<?php else: ?>
|
||||
<small class="text-muted">-</small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($mail['sender'] ?: $mail['recipient']) ?></td>
|
||||
<td>
|
||||
<?php if ($mail['assigned_to_name']): ?>
|
||||
<small><i class="fas fa-user-tag me-1 text-muted"></i> <?= htmlspecialchars($mail['assigned_to_name']) ?></small>
|
||||
<?php else: ?>
|
||||
<small class="text-muted">غير معين</small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= getStatusBadge($mail) ?></td>
|
||||
<td class="pe-4 text-center"><?= date('Y-m-d', strtotime($mail['date_registered'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
338
install.php
Executable file
338
install.php
Executable file
@ -0,0 +1,338 @@
|
||||
<?php
|
||||
session_start();
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
// Configuration file path
|
||||
$config_file = __DIR__ . '/db/config.php';
|
||||
$is_configured = file_exists($config_file);
|
||||
|
||||
// Step 0: Check requirements
|
||||
$requirements = [
|
||||
'PHP Version >= 7.4' => version_compare(PHP_VERSION, '7.4.0', '>='),
|
||||
'PDO Extension' => extension_loaded('pdo'),
|
||||
'PDO MySQL Extension' => extension_loaded('pdo_mysql'),
|
||||
'Config Directory Writable' => is_writable(__DIR__ . '/db/'),
|
||||
'Uploads Directory Writable' => is_writable(__DIR__ . '/uploads/') || (mkdir(__DIR__ . '/uploads/', 0777, true) && is_writable(__DIR__ . '/uploads/')),
|
||||
];
|
||||
|
||||
$all_requirements_met = !in_array(false, $requirements, true);
|
||||
|
||||
// Current step
|
||||
$step = isset($_GET['step']) ? (int)$_GET['step'] : 1;
|
||||
|
||||
// Handle form submissions
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if ($step === 2) {
|
||||
// Save database configuration
|
||||
$host = $_POST['db_host'] ?? '127.0.0.1';
|
||||
$name = $_POST['db_name'] ?? 'app_database';
|
||||
$user = $_POST['db_user'] ?? 'root';
|
||||
$password = $_POST['db_pass'] ?? '';
|
||||
|
||||
// Test connection
|
||||
try {
|
||||
$test_pdo = new PDO("mysql:host=$host;dbname=$name;charset=utf8mb4", $user, $password);
|
||||
$test_pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// Generate config file content
|
||||
$content = "<?php\n";
|
||||
$content .= "// Database configuration generated by installer\n";
|
||||
$content .= "define('DB_HOST', " . var_export($host, true) . ");\n";
|
||||
$content .= "define('DB_NAME', " . var_export($name, true) . ");\n";
|
||||
$content .= "define('DB_USER', " . var_export($user, true) . ");\n";
|
||||
$content .= "define('DB_PASS', " . var_export($password, true) . ");\n";
|
||||
$content .= "\n";
|
||||
$content .= "if (!function_exists('db')) {\n";
|
||||
$content .= " function db() {\n";
|
||||
$content .= " static \$pdo;\n";
|
||||
$content .= " if (!\$pdo) {\n";
|
||||
$content .= " try {\n";
|
||||
$content .= " \$pdo = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4', DB_USER, DB_PASS);
|
||||
";
|
||||
$content .= " \$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
";
|
||||
$content .= " \$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
";
|
||||
$content .= " \$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
|
||||
";
|
||||
$content .= " } catch (PDOException \$e) {\n";
|
||||
$content .= " die('Connection failed: ' . \$e->getMessage());\n";
|
||||
$content .= " }\n";
|
||||
$content .= " }\n";
|
||||
$content .= " return \$pdo;\n";
|
||||
$content .= " }\n";
|
||||
$content .= "}\n";
|
||||
|
||||
if (file_put_contents($config_file, $content)) {
|
||||
header('Location: ' . htmlspecialchars($_SERVER['SCRIPT_NAME']) . '?step=3');
|
||||
exit;
|
||||
} else {
|
||||
$error = "Failed to write configuration file to $config_file. Please check permissions.";
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
$error = "Connection failed: " . $e->getMessage();
|
||||
}
|
||||
} elseif ($step === 3) {
|
||||
// Run migrations
|
||||
if (!file_exists($config_file)) {
|
||||
$error = "Configuration file not found. Please go back to Step 2.";
|
||||
} else {
|
||||
try {
|
||||
require_once $config_file;
|
||||
if (!function_exists('db')) {
|
||||
throw new Exception("The 'db()' function is not defined in your config file.");
|
||||
}
|
||||
$pdo = db();
|
||||
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
|
||||
|
||||
// Create migration table if not exists
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS migrations (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
migration_name VARCHAR(255) NOT NULL UNIQUE,
|
||||
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)");
|
||||
|
||||
$migrations_dir = __DIR__ . '/db/migrations/';
|
||||
$files = glob($migrations_dir . '*.sql');
|
||||
if ($files === false) $files = [];
|
||||
sort($files);
|
||||
|
||||
$applied = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
$migration_name = basename($file);
|
||||
|
||||
// Check if already applied
|
||||
$stmt = $pdo->prepare("SELECT id FROM migrations WHERE migration_name = ?");
|
||||
$stmt->execute([$migration_name]);
|
||||
if ($stmt->fetch()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sql = file_get_contents($file);
|
||||
if (empty($sql)) continue;
|
||||
|
||||
// Split SQL into individual statements by ; followed by newline
|
||||
$statements = preg_split('/;(?:\\s*[
|
||||
]+)/', $sql);
|
||||
|
||||
$file_success = true;
|
||||
foreach ($statements as $stmt_sql) {
|
||||
$stmt_sql = trim($stmt_sql);
|
||||
if (empty($stmt_sql)) continue;
|
||||
|
||||
// Basic comment removal
|
||||
$stmt_lines = explode("\n", $stmt_sql);
|
||||
$clean_stmt = "";
|
||||
foreach ($stmt_lines as $line) {
|
||||
if (trim(substr(trim($line), 0, 2)) === '--') continue;
|
||||
$clean_stmt .= $line . "\n";
|
||||
}
|
||||
$clean_stmt = trim($clean_stmt);
|
||||
if (empty($clean_stmt)) continue;
|
||||
|
||||
try {
|
||||
$res = $pdo->query($clean_stmt);
|
||||
if ($res) {
|
||||
$res->closeCursor();
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$msg = $e->getMessage();
|
||||
// If the error is about a table already existing, it's fine
|
||||
if (strpos($msg, "already exists") !== false ||
|
||||
strpos($msg, "Duplicate column") !== false ||
|
||||
strpos($msg, "Duplicate entry") !== false ||
|
||||
strpos($msg, "already a column") !== false ||
|
||||
strpos($msg, "Duplicate key") !== false ||
|
||||
strpos($msg, "errno: 121") !== false) {
|
||||
continue;
|
||||
} else {
|
||||
$errors[] = $migration_name . " at statement: " . substr($clean_stmt, 0, 50) . "... Error: " . $msg;
|
||||
$file_success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($file_success) {
|
||||
$ins = $pdo->prepare("INSERT INTO migrations (migration_name) VALUES (?)");
|
||||
$ins->execute([$migration_name]);
|
||||
$applied++;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($errors)) {
|
||||
$success = "Successfully applied migrations.";
|
||||
header('Location: ' . htmlspecialchars($_SERVER['SCRIPT_NAME']) . '?step=4');
|
||||
exit;
|
||||
} else {
|
||||
$error = "Applied migrations, but some errors occurred:<br><ul><li>" . implode('</li><li>', $errors) . "</li></ul>";
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$error = "Migration failed: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
} elseif ($step === 4) {
|
||||
// Final setup (Admin account)
|
||||
require_once $config_file;
|
||||
$pdo = db();
|
||||
|
||||
$admin_user = $_POST['admin_user'] ?? 'admin';
|
||||
$admin_pass = $_POST['admin_pass'] ?? '';
|
||||
$admin_email = $_POST['admin_email'] ?? 'admin@example.com';
|
||||
|
||||
if (strlen($admin_pass) < 6) {
|
||||
$error = "Password must be at least 6 characters long.";
|
||||
} else {
|
||||
try {
|
||||
$hashed_pass = password_hash($admin_pass, PASSWORD_DEFAULT);
|
||||
$stmt = $pdo->prepare("INSERT INTO users (username, password, email, role) VALUES (?, ?, ?, 'admin')
|
||||
ON DUPLICATE KEY UPDATE password = ?, email = ?");
|
||||
$stmt->execute([$admin_user, $hashed_pass, $admin_email, $hashed_pass, $admin_email]);
|
||||
|
||||
// Set initial settings
|
||||
$pdo->exec("INSERT IGNORE INTO charity_settings (id, charity_name) VALUES (1, 'Admin Panel')");
|
||||
$pdo->exec("INSERT IGNORE INTO smtp_settings (id, is_enabled) VALUES (1, 0)");
|
||||
|
||||
header('Location: ' . htmlspecialchars($_SERVER['SCRIPT_NAME']) . '?step=5');
|
||||
exit;
|
||||
} catch (Throwable $e) {
|
||||
$error = "Failed to create admin account: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UI Template
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Installer - Step <?= $step ?></title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background-color: #f8f9fa; padding-top: 50px; }
|
||||
.installer-card { max-width: 600px; margin: 0 auto; box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); border-radius: 1rem; }
|
||||
.step-indicator { margin-bottom: 2rem; }
|
||||
.step-dot { width: 30px; height: 30px; border-radius: 50%; background: #dee2e6; display: inline-block; text-align: center; line-height: 30px; font-weight: bold; margin: 0 5px; }
|
||||
.step-dot.active { background: #0d6efd; color: white; }
|
||||
.step-dot.completed { background: #198754; color: white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card installer-card">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="text-center mb-4">Installer</h2>
|
||||
|
||||
<div class="text-center step-indicator">
|
||||
<?php for ($i = 1; $i <= 5; $i++):
|
||||
$class = ($i == $step) ? 'active' : (($i < $step) ? 'completed' : '');
|
||||
echo "<span class=\"step-dot $class\">$i</span>\n";
|
||||
endfor; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger"><?= $error ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?= $success ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($step === 1): ?>
|
||||
<h4>Step 1: System Requirements</h4>
|
||||
<ul class="list-group mb-4">
|
||||
<?php foreach ($requirements as $name => $met):
|
||||
echo "<li class=\"list-group-item d-flex justify-content-between align-items-center\">";
|
||||
echo "$name\n";
|
||||
if ($met) {
|
||||
echo "<span class=\"badge bg-success rounded-pill\">OK</span>";
|
||||
} else {
|
||||
echo "<span class=\"badge bg-danger rounded-pill\">Failed</span>";
|
||||
}
|
||||
echo "</li>\n";
|
||||
endforeach; ?>
|
||||
</ul>
|
||||
<div class="d-grid">
|
||||
<?php if ($all_requirements_met): ?>
|
||||
<a href="<?= htmlspecialchars($_SERVER['SCRIPT_NAME']) ?>?step=2" class="btn btn-primary">Next: Database Config</a>
|
||||
<?php else:
|
||||
echo "<button class=\"btn btn-secondary\" disabled>Fix requirements to continue</button>";
|
||||
endif; ?>
|
||||
</div>
|
||||
|
||||
<?php elseif ($step === 2): ?>
|
||||
<h4>Step 2: Database Connection</h4>
|
||||
<form method="POST" action="<?= htmlspecialchars($_SERVER['SCRIPT_NAME']) ?>?step=2">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Database Host</label>
|
||||
<input type="text" name="db_host" class="form-control" value="127.0.0.1" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Database Name</label>
|
||||
<input type="text" name="db_name" class="form-control" value="app_database" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Database User</label>
|
||||
<input type="text" name="db_user" class="form-control" value="root" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Database Password</label>
|
||||
<input type="password" name="db_pass" class="form-control">
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Test & Save Config</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php elseif ($step === 3): ?>
|
||||
<h4>Step 3: Database Migrations</h4>
|
||||
<p>We will now run the SQL scripts to set up your database tables.</p>
|
||||
<form method="POST" action="<?= htmlspecialchars($_SERVER['SCRIPT_NAME']) ?>?step=3">
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Run Migrations</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php elseif ($step === 4): ?>
|
||||
<h4>Step 4: Admin Account</h4>
|
||||
<form method="POST" action="<?= htmlspecialchars($_SERVER['SCRIPT_NAME']) ?>?step=4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Admin Username</label>
|
||||
<input type="text" name="admin_user" class="form-control" value="admin" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Admin Email</label>
|
||||
<input type="email" name="admin_email" class="form-control" value="admin@example.com" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Admin Password</label>
|
||||
<input type="password" name="admin_pass" class="form-control" required minlength="6">
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Complete Setup</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php elseif ($step === 5): ?>
|
||||
<div class="text-center">
|
||||
<h4 class="text-success">Installation Complete!</h4>
|
||||
<p>The system is ready to use. For security, please delete <b>install.php</b> or rename it.</p>
|
||||
<div class="d-grid">
|
||||
<a href="login.php" class="btn btn-primary">Go to Login</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
175
internal_inbox.php
Normal file
175
internal_inbox.php
Normal file
@ -0,0 +1,175 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Every logged-in user can access their own internal mail if they have permission
|
||||
if (!canView('internal')) {
|
||||
redirect('index.php');
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$success = $_GET['success'] ?? '';
|
||||
$error = $_GET['error'] ?? '';
|
||||
|
||||
// Search and filtering
|
||||
$search = $_GET['search'] ?? '';
|
||||
$page = isset($_GET['page']) && is_numeric($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$params = [$user_id];
|
||||
$where = "1=1 AND m.assigned_to = ?";
|
||||
|
||||
if ($search) {
|
||||
$where .= " AND (m.subject LIKE ? OR m.description LIKE ? OR u_sender.full_name LIKE ?)";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
}
|
||||
|
||||
// Get total for pagination
|
||||
$count_stmt = db()->prepare("SELECT COUNT(*) FROM internal_mail m LEFT JOIN users u_sender ON m.created_by = u_sender.id WHERE $where");
|
||||
$count_stmt->execute($params);
|
||||
$total_records = $count_stmt->fetchColumn();
|
||||
$total_pages = ceil($total_records / $limit);
|
||||
|
||||
// Fetch messages
|
||||
$query = "SELECT m.*, u_sender.full_name as sender_name, u_sender.profile_image as sender_image, s.name as status_name, s.color as status_color,
|
||||
(SELECT GROUP_CONCAT(display_name SEPARATOR ', ') FROM internal_attachments WHERE mail_id = m.id) as attachment_names
|
||||
FROM internal_mail m
|
||||
LEFT JOIN users u_sender ON m.created_by = u_sender.id
|
||||
LEFT JOIN mailbox_statuses s ON m.status_id = s.id
|
||||
WHERE $where
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT $limit OFFSET $offset";
|
||||
|
||||
$stmt = db()->prepare($query);
|
||||
$stmt->execute($params);
|
||||
$messages = $stmt->fetchAll();
|
||||
|
||||
function getStatusBadgeInternal($mail) {
|
||||
$status_name = $mail['status_name'] ?? 'received';
|
||||
$status_color = $mail['status_color'] ?? '#6c757d';
|
||||
|
||||
$display_name = $status_name;
|
||||
if ($status_name == 'received') $display_name = 'جديد';
|
||||
if ($status_name == 'in_progress') $display_name = 'قيد المعالجة';
|
||||
if ($status_name == 'closed') $display_name = 'مؤرشف';
|
||||
|
||||
return '<span class="badge" style="background-color: ' . $status_color . ';">' . htmlspecialchars($display_name) . '</span>';
|
||||
}
|
||||
?>
|
||||
|
||||
<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"><i class="fas fa-inbox me-2 text-primary"></i> بريد الموظفين - الوارد</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<?php if (canAdd('internal')): ?>
|
||||
<a href="internal_outbox.php?action=compose" class="btn btn-primary shadow-sm">
|
||||
<i class="fas fa-paper-plane me-1"></i> إرسال رسالة جديدة
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<?= htmlspecialchars($success) ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<form class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0"><i class="fas fa-search text-muted"></i></span>
|
||||
<input type="text" name="search" class="form-control border-start-0" placeholder="بحث في الموضوع، الرسالة، أو المرسل..." value="<?= htmlspecialchars($search) ?>">
|
||||
<button type="submit" class="btn btn-primary">بحث</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($search): ?>
|
||||
<div class="col-auto">
|
||||
<a href="internal_inbox.php" class="btn btn-outline-secondary">إعادة تعيين</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
<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 class="pe-4 text-center">الإجراء</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($messages): ?>
|
||||
<?php foreach ($messages as $msg): ?>
|
||||
<tr style="cursor: pointer;" onclick="window.location='view_mail.php?id=<?= $msg['id'] ?>&type=internal'">
|
||||
<td class="ps-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<?php if ($msg['sender_image']): ?>
|
||||
<img src="<?= $msg['sender_image'] ?>" class="rounded-circle me-2" width="32" height="32" style="object-fit: cover;">
|
||||
<?php else: ?>
|
||||
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center me-2" width="32" height="32" style="width:32px; height:32px;">
|
||||
<i class="fas fa-user text-secondary small"></i>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<span class="fw-bold"><?= htmlspecialchars($msg['sender_name'] ?: 'مستخدم غير معروف') ?></span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold"><?= htmlspecialchars($msg['subject']) ?></div>
|
||||
<small class="text-muted text-truncate d-inline-block" style="max-width: 300px;">
|
||||
<?= strip_tags($msg['description']) ?>
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!empty($msg['attachment_names'])): ?>
|
||||
<small class="text-muted"><i class="fas fa-paperclip me-1"></i> <?= htmlspecialchars($msg['attachment_names']) ?></small>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">-</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted"><?= date('Y-m-d H:i', strtotime($msg['created_at'])) ?></small>
|
||||
</td>
|
||||
<td><?= getStatusBadgeInternal($msg) ?></td>
|
||||
<td class="pe-4 text-center">
|
||||
<a href="view_mail.php?id=<?= $msg['id'] ?>&type=internal" class="btn btn-sm btn-light rounded-pill px-3">عرض</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-5 text-muted">
|
||||
<i class="fas fa-envelope-open fa-3x mb-3 opacity-25"></i>
|
||||
<p>لا توجد رسائل واردة حالياً</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($total_pages > 1): ?>
|
||||
<div class="card-footer bg-white border-0 py-3">
|
||||
<nav>
|
||||
<ul class="pagination justify-content-center mb-0">
|
||||
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
|
||||
<li class="page-item <?= ($page == $i) ? 'active' : '' ?>">
|
||||
<a class="page-link" href="?page=<?= $i ?><?= $search ? '&search='.urlencode($search) : '' ?>"><?= $i ?></a>
|
||||
</li>
|
||||
<?php endfor; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
382
internal_outbox.php
Normal file
382
internal_outbox.php
Normal file
@ -0,0 +1,382 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
require_once __DIR__ . '/m_services/MailService.php';
|
||||
|
||||
if (!canView('internal')) {
|
||||
redirect('index.php');
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$success = '';
|
||||
$error = '';
|
||||
|
||||
// Handle composing a new message
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'compose') {
|
||||
if (!canAdd('internal')) {
|
||||
$error = 'عذراً، ليس لديك الصلاحية لإرسال رسائل';
|
||||
} else {
|
||||
$recipient_id = !empty($_POST['recipient_id']) ? $_POST['recipient_id'] : null;
|
||||
$subject = $_POST['subject'] ?? '';
|
||||
$description = $_POST['description'] ?? '';
|
||||
$type = 'internal';
|
||||
$ref_no = generateRefNo('internal');
|
||||
$date_registered = date('Y-m-d');
|
||||
$default_status_id = db()->query("SELECT id FROM mailbox_statuses WHERE is_default = 1 LIMIT 1")->fetchColumn() ?: 1;
|
||||
|
||||
if ($recipient_id && $subject) {
|
||||
$should_notify = false;
|
||||
$recipient_email = '';
|
||||
try {
|
||||
db()->beginTransaction();
|
||||
$stmt = db()->prepare("INSERT INTO internal_mail (ref_no, date_registered, subject, description, status_id, assigned_to, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$ref_no, $date_registered, $subject, $description, $default_status_id, $recipient_id, $user_id]);
|
||||
$mail_id = db()->lastInsertId();
|
||||
|
||||
// Handle Attachments
|
||||
if (!empty($_FILES['attachments']['name'][0])) {
|
||||
$upload_dir = 'uploads/attachments/';
|
||||
if (!is_dir($upload_dir)) mkdir($upload_dir, 0777, true);
|
||||
|
||||
foreach ($_FILES['attachments']['name'] as $key => $name) {
|
||||
if ($_FILES['attachments']['error'][$key] === 0) {
|
||||
$file_name = time() . '_' . basename($name);
|
||||
$target_path = $upload_dir . $file_name;
|
||||
if (move_uploaded_file($_FILES['attachments']['tmp_name'][$key], $target_path)) {
|
||||
$stmt = db()->prepare("INSERT INTO internal_attachments (mail_id, display_name, file_path, file_name, file_size) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$mail_id, $name, $target_path, $name, $_FILES['attachments']['size'][$key]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get recipient info for notification
|
||||
$stmt_recp = db()->prepare("SELECT full_name, email FROM users WHERE id = ?");
|
||||
$stmt_recp->execute([$recipient_id]);
|
||||
$recipient = $stmt_recp->fetch();
|
||||
|
||||
if ($recipient && !empty($recipient['email'])) {
|
||||
$should_notify = true;
|
||||
$recipient_email = $recipient['email'];
|
||||
}
|
||||
|
||||
db()->commit();
|
||||
|
||||
// Notify after commit
|
||||
if ($should_notify) {
|
||||
$email_subject = "رسالة داخلية جديدة من " . $_SESSION['name'];
|
||||
$htmlBody = "
|
||||
<div dir='rtl' style='font-family: Arial, sans-serif;'>
|
||||
<h3>لديك رسالة داخلية جديدة</h3>
|
||||
<p><strong>الموضوع:</strong> " . htmlspecialchars($subject) . "</p>
|
||||
<p><strong>المرسل:</strong> " . htmlspecialchars($_SESSION['name']) . "</p>
|
||||
<hr>
|
||||
<div>" . $description . "</div>
|
||||
<br>
|
||||
<p>يمكنك الرد من خلال النظام.</p>
|
||||
</div>
|
||||
";
|
||||
MailService::sendMail($recipient_email, $email_subject, $htmlBody);
|
||||
}
|
||||
|
||||
$_SESSION['success'] = 'تم إرسال الرسالة بنجاح';
|
||||
redirect('internal_outbox.php');
|
||||
} catch (PDOException $e) {
|
||||
if (db()->inTransaction()) db()->rollBack();
|
||||
$error = 'حدث خطأ: ' . $e->getMessage();
|
||||
}
|
||||
} else {
|
||||
$error = 'يرجى اختيار المرسل إليه وكتابة الموضوع';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get session messages
|
||||
if (isset($_SESSION['success'])) {
|
||||
$success = $_SESSION['success'];
|
||||
unset($_SESSION['success']);
|
||||
}
|
||||
if (isset($_SESSION['error'])) {
|
||||
$error = $_SESSION['error'];
|
||||
unset($_SESSION['error']);
|
||||
}
|
||||
|
||||
// Search and filtering
|
||||
$search = $_GET['search'] ?? '';
|
||||
$page = isset($_GET['page']) && is_numeric($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$params = [$user_id];
|
||||
$where = "1=1 AND m.created_by = ?";
|
||||
|
||||
if ($search) {
|
||||
$where .= " AND (m.subject LIKE ? OR m.description LIKE ? OR u_recp.full_name LIKE ?)";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
}
|
||||
|
||||
// Get total for pagination
|
||||
$count_stmt = db()->prepare("SELECT COUNT(*) FROM internal_mail m LEFT JOIN users u_recp ON m.assigned_to = u_recp.id WHERE $where");
|
||||
$count_stmt->execute($params);
|
||||
$total_records = $count_stmt->fetchColumn();
|
||||
$total_pages = ceil($total_records / $limit);
|
||||
|
||||
// Fetch messages
|
||||
$query = "SELECT m.*, u_recp.full_name as recipient_name, u_recp.profile_image as recipient_image, s.name as status_name, s.color as status_color,
|
||||
(SELECT GROUP_CONCAT(display_name SEPARATOR ', ') FROM internal_attachments WHERE mail_id = m.id) as attachment_names
|
||||
FROM internal_mail m
|
||||
LEFT JOIN users u_recp ON m.assigned_to = u_recp.id
|
||||
LEFT JOIN mailbox_statuses s ON m.status_id = s.id
|
||||
WHERE $where
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT $limit OFFSET $offset";
|
||||
|
||||
$stmt = db()->prepare($query);
|
||||
$stmt->execute($params);
|
||||
$messages = $stmt->fetchAll();
|
||||
|
||||
// Users for compose
|
||||
$users_list = db()->prepare("SELECT id, full_name, username FROM users WHERE id != ? ORDER BY full_name");
|
||||
$users_list->execute([$user_id]);
|
||||
$users_list = $users_list->fetchAll();
|
||||
|
||||
function getStatusBadgeInternal($mail) {
|
||||
$status_name = $mail['status_name'] ?? 'received';
|
||||
$status_color = $mail['status_color'] ?? '#6c757d';
|
||||
|
||||
$display_name = $status_name;
|
||||
if ($status_name == 'received') $display_name = 'مرسلة';
|
||||
if ($status_name == 'in_progress') $display_name = 'قيد المتابعة';
|
||||
if ($status_name == 'closed') $display_name = 'مؤرشفة';
|
||||
|
||||
return '<span class="badge" style="background-color: ' . $status_color . ';">' . htmlspecialchars($display_name) . '</span>';
|
||||
}
|
||||
?>
|
||||
|
||||
<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"><i class="fas fa-paper-plane me-2 text-success"></i> بريد الموظفين - الصادر</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<?php if (canAdd('internal')): ?>
|
||||
<button type="button" class="btn btn-primary shadow-sm" data-bs-toggle="modal" data-bs-target="#composeModal">
|
||||
<i class="fas fa-plus-circle me-1"></i> إنشاء رسالة جديدة
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<?= htmlspecialchars($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">
|
||||
<?= htmlspecialchars($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 mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<form class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-end-0"><i class="fas fa-search text-muted"></i></span>
|
||||
<input type="text" name="search" class="form-control border-start-0" placeholder="بحث في الموضوع، الرسالة، أو المستلم..." value="<?= htmlspecialchars($search) ?>">
|
||||
<button type="submit" class="btn btn-primary">بحث</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($search): ?>
|
||||
<div class="col-auto">
|
||||
<a href="internal_outbox.php" class="btn btn-outline-secondary">إعادة تعيين</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
<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 class="pe-4 text-center">الإجراء</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($messages): ?>
|
||||
<?php foreach ($messages as $msg): ?>
|
||||
<tr style="cursor: pointer;" onclick="window.location='view_mail.php?id=<?= $msg['id'] ?>&type=internal'">
|
||||
<td class="ps-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<?php if ($msg['recipient_image']): ?>
|
||||
<img src="<?= $msg['recipient_image'] ?>" class="rounded-circle me-2" width="32" height="32" style="object-fit: cover;">
|
||||
<?php else: ?>
|
||||
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center me-2" width="32" height="32" style="width:32px; height:32px;">
|
||||
<i class="fas fa-user text-secondary small"></i>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<span class="fw-bold"><?= htmlspecialchars($msg['recipient_name'] ?: 'مستخدم غير معروف') ?></span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold"><?= htmlspecialchars($msg['subject']) ?></div>
|
||||
<small class="text-muted text-truncate d-inline-block" style="max-width: 300px;">
|
||||
<?= strip_tags($msg['description']) ?>
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!empty($msg['attachment_names'])): ?>
|
||||
<small class="text-muted"><i class="fas fa-paperclip me-1"></i> <?= htmlspecialchars($msg['attachment_names']) ?></small>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">-</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted"><?= date('Y-m-d H:i', strtotime($msg['created_at'])) ?></small>
|
||||
</td>
|
||||
<td><?= getStatusBadgeInternal($msg) ?></td>
|
||||
<td class="pe-4 text-center">
|
||||
<a href="view_mail.php?id=<?= $msg['id'] ?>&type=internal" class="btn btn-sm btn-light rounded-pill px-3">عرض</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-5 text-muted">
|
||||
<i class="fas fa-paper-plane fa-3x mb-3 opacity-25"></i>
|
||||
<p>لم يتم إرسال أي رسائل حالياً</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($total_pages > 1): ?>
|
||||
<div class="card-footer bg-white border-0 py-3">
|
||||
<nav>
|
||||
<ul class="pagination justify-content-center mb-0">
|
||||
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
|
||||
<li class="page-item <?= ($page == $i) ? 'active' : '' ?>">
|
||||
<a class="page-link" href="?page=<?= $i ?><?= $search ? '&search='.urlencode($search) : '' ?>"><?= $i ?></a>
|
||||
</li>
|
||||
<?php endfor; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Compose Modal -->
|
||||
<div class="modal fade" id="composeModal" tabindex="-1" aria-labelledby="composeModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content border-0 shadow-lg">
|
||||
<div class="modal-header bg-dark text-white border-0">
|
||||
<h5 class="modal-title fw-bold" id="composeModalLabel">إنشاء رسالة جديدة للموظفين</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="composeForm" method="POST" enctype="multipart/form-data">
|
||||
<div class="modal-body p-4">
|
||||
<input type="hidden" name="action" value="compose">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold">إلى <span class="text-danger">*</span></label>
|
||||
<select name="recipient_id" class="form-select border-2" required>
|
||||
<option value="">-- اختر الموظف --</option>
|
||||
<?php foreach ($users_list as $u): ?>
|
||||
<option value="<?= $u['id'] ?>"><?= htmlspecialchars($u['full_name'] . ' (@' . $u['username'] . ')') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold">الموضوع <span class="text-danger">*</span></label>
|
||||
<input type="text" name="subject" class="form-control border-2" required placeholder="عنوان الرسالة">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold">الرسالة</label>
|
||||
<textarea name="description" id="composeEditor" class="form-control border-2" rows="10"></textarea>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold">المرفقات</label>
|
||||
<input type="file" name="attachments[]" id="composeAttachmentsInput" class="form-control border-2" multiple>
|
||||
<div id="composeSelectedAttachments" class="mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-top-0 bg-light p-3">
|
||||
<button type="button" class="btn btn-secondary px-4 py-2" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary px-5 py-2 fw-bold">إرسال الآن <i class="fas fa-paper-plane ms-2"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (typeof tinymce !== 'undefined') {
|
||||
tinymce.init({
|
||||
selector: '#composeEditor',
|
||||
language: 'ar', language_url: 'https://cdn.jsdelivr.net/npm/tinymce-i18n@23.10.9/langs6/ar.js',
|
||||
directionality: 'rtl',
|
||||
height: 400,
|
||||
plugins: 'advlist autolink lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table help wordcount',
|
||||
toolbar: 'undo redo | fontfamily fontsize | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | help',
|
||||
font_size_formats: '8pt 10pt 12pt 14pt 18pt 24pt 36pt',
|
||||
promotion: false,
|
||||
branding: false
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('composeForm').addEventListener('submit', function() {
|
||||
if (typeof tinymce !== 'undefined') {
|
||||
tinymce.triggerSave();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle file selection display
|
||||
const attachmentsInput = document.getElementById('composeAttachmentsInput');
|
||||
if (attachmentsInput) {
|
||||
attachmentsInput.addEventListener('change', function() {
|
||||
const fileList = this.files;
|
||||
const selectedAttachmentsDiv = document.getElementById('composeSelectedAttachments');
|
||||
selectedAttachmentsDiv.innerHTML = '';
|
||||
|
||||
if (fileList.length > 0) {
|
||||
let html = '<div class="mt-2"><p class="mb-1 fw-bold small text-primary">المرفقات المختارة للرفع:</p><ul class="list-unstyled small">';
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const fileSize = (fileList[i].size / 1024).toFixed(1);
|
||||
html += `<li><i class="fas fa-file-upload me-1 text-primary"></i> ${fileList[i].name} <span class="text-muted">(${fileSize} KB)</span></li>`;
|
||||
}
|
||||
html += '</ul></div>';
|
||||
selectedAttachmentsDiv.innerHTML = html;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Reset attachments list when modal is hidden
|
||||
const composeModal = document.getElementById('composeModal');
|
||||
if (composeModal) {
|
||||
composeModal.addEventListener('hidden.bs.modal', function () {
|
||||
const selectedAttachmentsDiv = document.getElementById('composeSelectedAttachments');
|
||||
const attachmentsInput = document.getElementById('composeAttachmentsInput');
|
||||
if (selectedAttachmentsDiv) selectedAttachmentsDiv.innerHTML = '';
|
||||
if (attachmentsInput) attachmentsInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
<?php if (isset($_GET['action']) && $_GET['action'] === 'compose'): ?>
|
||||
var myModal = new bootstrap.Modal(document.getElementById('composeModal'));
|
||||
myModal.show();
|
||||
<?php endif; ?>
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
86
login.php
Normal file
86
login.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
if (isLoggedIn()) {
|
||||
redirect('user_dashboard.php');
|
||||
}
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if ($username && $password) {
|
||||
$stmt = db()->prepare("SELECT * FROM users WHERE username = ?");
|
||||
$stmt->execute([$username]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user && password_verify($password, $user['password'])) {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
$_SESSION['role'] = $user['role'];
|
||||
$_SESSION['is_super_admin'] = (int)$user['is_super_admin'];
|
||||
|
||||
// Redirect to dashboard
|
||||
redirect('user_dashboard.php');
|
||||
} else {
|
||||
$error = "اسم المستخدم أو كلمة المرور غير صحيحة";
|
||||
}
|
||||
} else {
|
||||
$error = "يرجى إدخال اسم المستخدم وكلمة المرور";
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="row justify-content-center align-items-center" style="min-height: 80vh;">
|
||||
<div class="col-md-4 col-lg-3">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<div class="text-center mb-4">
|
||||
<a href="index.php" class="text-decoration-none text-dark d-block">
|
||||
<?php if (!empty($sys_settings['site_logo'])): ?>
|
||||
<img src="<?php echo htmlspecialchars($sys_settings['site_logo']); ?>" alt="Logo" class="img-fluid mb-3" style="max-height: 80px;">
|
||||
<?php endif; ?>
|
||||
<h4 class="fw-bold mb-0"><?php echo htmlspecialchars($sys_settings['site_name']); ?></h4>
|
||||
<p class="text-danger mb-0 fw-bold">نظام إدارة الفرق التطوعية ولجان الزكاة</p>
|
||||
</a>
|
||||
<p class="text-muted small">يرجى إدخال بيانات الاعتماد الخاصة بك</p>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger py-2 small" role="alert">
|
||||
<?php echo $error; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" action="">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label small fw-semibold">اسم المستخدم</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required tabindex="1" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<label for="password" class="form-label small fw-semibold mb-0">كلمة المرور</label>
|
||||
<a href="forgot_password.php" class="text-decoration-none small text-primary" tabindex="4">نسيت كلمة المرور؟</a>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="password" name="password" required tabindex="2">
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary w-100 fw-bold py-2" tabindex="3">تسجيل الدخول</button>
|
||||
</div>
|
||||
|
||||
<?php if ($sys_settings['allow_registration']): ?>
|
||||
<div class="mt-3 text-center">
|
||||
<p class="small text-muted">ليس لديك حساب؟ <a href="register.php" class="text-decoration-none text-primary">إنشاء حساب جديد</a></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
6
logout.php
Normal file
6
logout.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
session_start();
|
||||
session_unset();
|
||||
session_destroy();
|
||||
header("Location: login.php");
|
||||
exit;
|
||||
191
m_services/MailService.php
Normal file
191
m_services/MailService.php
Normal file
@ -0,0 +1,191 @@
|
||||
<?php
|
||||
// Minimal mail service for the workspace app (VM).
|
||||
// Usage:
|
||||
// require_once __DIR__ . '/MailService.php';
|
||||
// // Generic:
|
||||
// MailService::sendMail($to, $subject, $htmlBody, $textBody = null, $opts = []);
|
||||
// // Contact form helper:
|
||||
// MailService::sendContactMessage($name, $email, $message, $to = null, $subject = 'New contact form');
|
||||
|
||||
class MailService
|
||||
{
|
||||
// Universal mail sender (no attachments by design)
|
||||
public static function sendMail($to, string $subject, string $htmlBody, ?string $textBody = null, array $opts = [])
|
||||
{
|
||||
$cfg = self::loadConfig();
|
||||
|
||||
// Check if enabled (db-backed settings)
|
||||
if (isset($cfg['is_enabled']) && !$cfg['is_enabled']) {
|
||||
self::logEmail($to, $subject, 'failure', 'SMTP is currently disabled due to repeated failures.');
|
||||
return [ 'success' => false, 'error' => 'SMTP is disabled' ];
|
||||
}
|
||||
|
||||
$autoload = __DIR__ . '/../vendor/autoload.php';
|
||||
if (file_exists($autoload)) {
|
||||
require_once $autoload;
|
||||
}
|
||||
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
|
||||
@require_once 'libphp-phpmailer/autoload.php';
|
||||
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
|
||||
@require_once 'libphp-phpmailer/src/Exception.php';
|
||||
@require_once 'libphp-phpmailer/src/SMTP.php';
|
||||
@require_once 'libphp-phpmailer/src/PHPMailer.php';
|
||||
}
|
||||
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
|
||||
@require_once 'PHPMailer/src/Exception.php';
|
||||
@require_once 'PHPMailer/src/SMTP.php';
|
||||
@require_once 'PHPMailer/src/PHPMailer.php';
|
||||
}
|
||||
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
|
||||
@require_once 'PHPMailer/Exception.php';
|
||||
@require_once 'PHPMailer/SMTP.php';
|
||||
@require_once 'PHPMailer/PHPMailer.php';
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
|
||||
self::logEmail($to, $subject, 'failure', 'PHPMailer not available');
|
||||
return [ 'success' => false, 'error' => 'PHPMailer not available' ];
|
||||
}
|
||||
|
||||
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
|
||||
try {
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $cfg['smtp_host'] ?? '';
|
||||
$mail->Port = (int)($cfg['smtp_port'] ?? 587);
|
||||
$secure = $cfg['smtp_secure'] ?? 'tls';
|
||||
if ($secure === 'ssl') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS;
|
||||
elseif ($secure === 'tls') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
|
||||
else $mail->SMTPSecure = false;
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $cfg['smtp_user'] ?? '';
|
||||
$mail->Password = $cfg['smtp_pass'] ?? '';
|
||||
|
||||
// Set timeout to 10 seconds to prevent long hangs
|
||||
$mail->Timeout = 10;
|
||||
$mail->SMTPKeepAlive = false;
|
||||
|
||||
$fromEmail = $opts['from_email'] ?? ($cfg['from_email'] ?? 'no-reply@localhost');
|
||||
$fromName = $opts['from_name'] ?? ($cfg['from_name'] ?? 'App');
|
||||
$mail->setFrom($fromEmail, $fromName);
|
||||
if (!empty($opts['reply_to']) && filter_var($opts['reply_to'], FILTER_VALIDATE_EMAIL)) {
|
||||
$mail->addReplyTo($opts['reply_to']);
|
||||
} elseif (!empty($cfg['reply_to'])) {
|
||||
$mail->addReplyTo($cfg['reply_to']);
|
||||
}
|
||||
|
||||
// Recipients
|
||||
$toList = [];
|
||||
if ($to) {
|
||||
if (is_string($to)) $toList = array_map('trim', explode(',', $to));
|
||||
elseif (is_array($to)) $toList = $to;
|
||||
} elseif (!empty(getenv('MAIL_TO'))) {
|
||||
$toList = array_map('trim', explode(',', getenv('MAIL_TO')));
|
||||
}
|
||||
$added = 0;
|
||||
foreach ($toList as $addr) {
|
||||
if (filter_var($addr, FILTER_VALIDATE_EMAIL)) { $mail->addAddress($addr); $added++; }
|
||||
}
|
||||
if ($added === 0) {
|
||||
self::logEmail($to, $subject, 'failure', 'No recipients defined');
|
||||
return [ 'success' => false, 'error' => 'No recipients defined (set MAIL_TO or pass $to)' ];
|
||||
}
|
||||
|
||||
foreach ((array)($opts['cc'] ?? []) as $cc) { if (filter_var($cc, FILTER_VALIDATE_EMAIL)) $mail->addCC($cc); }
|
||||
foreach ((array)($opts['bcc'] ?? []) as $bcc){ if (filter_var($bcc, FILTER_VALIDATE_EMAIL)) $mail->addBCC($bcc); }
|
||||
|
||||
// Optional DKIM
|
||||
if (!empty($cfg['dkim_domain']) && !empty($cfg['dkim_selector']) && !empty($cfg['dkim_private_key_path'])) {
|
||||
$mail->DKIM_domain = $cfg['dkim_domain'];
|
||||
$mail->DKIM_selector = $cfg['dkim_selector'];
|
||||
$mail->DKIM_private = $cfg['dkim_private_key_path'];
|
||||
}
|
||||
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $htmlBody;
|
||||
$mail->AltBody = $textBody ?? strip_tags($htmlBody);
|
||||
$ok = $mail->send();
|
||||
|
||||
self::logEmail($to, $subject, 'success');
|
||||
self::resetFailures();
|
||||
|
||||
return [ 'success' => $ok ];
|
||||
} catch (\Throwable $e) {
|
||||
$error = $e->getMessage();
|
||||
self::logEmail($to, $subject, 'failure', $error);
|
||||
self::incrementFailures();
|
||||
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $error ];
|
||||
}
|
||||
}
|
||||
|
||||
private static function loadConfig(): array
|
||||
{
|
||||
$configPath = __DIR__ . '/config.php';
|
||||
if (!file_exists($configPath)) {
|
||||
throw new \RuntimeException('Mail config not found.');
|
||||
}
|
||||
$cfg = require $configPath;
|
||||
if (!is_array($cfg)) {
|
||||
throw new \RuntimeException('Invalid mail config format.');
|
||||
}
|
||||
|
||||
// Try to load extra from DB
|
||||
try {
|
||||
$stmt = db()->query("SELECT * FROM smtp_settings LIMIT 1");
|
||||
$dbCfg = $stmt->fetch();
|
||||
if ($dbCfg) {
|
||||
// Merge DB settings if not set in config or env
|
||||
foreach ($dbCfg as $key => $val) {
|
||||
if (!isset($cfg[$key])) $cfg[$key] = $val;
|
||||
}
|
||||
// Specifically override enablement
|
||||
$cfg['is_enabled'] = (bool)$dbCfg['is_enabled'];
|
||||
$cfg['max_failures'] = (int)$dbCfg['max_failures'];
|
||||
}
|
||||
} catch (\Exception $e) {}
|
||||
|
||||
return $cfg;
|
||||
}
|
||||
|
||||
private static function logEmail($to, $subject, $status, $error = null)
|
||||
{
|
||||
try {
|
||||
if (is_array($to)) $to = implode(', ', $to);
|
||||
$stmt = db()->prepare("INSERT INTO email_logs (recipient, subject, status, error_message) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$to, $subject, $status, $error]);
|
||||
} catch (\Exception $e) {}
|
||||
}
|
||||
|
||||
private static function incrementFailures()
|
||||
{
|
||||
try {
|
||||
db()->query("UPDATE smtp_settings SET consecutive_failures = consecutive_failures + 1 WHERE id = 1");
|
||||
// Check if threshold reached
|
||||
$stmt = db()->query("SELECT consecutive_failures, max_failures FROM smtp_settings WHERE id = 1");
|
||||
$res = $stmt->fetch();
|
||||
if ($res && $res['consecutive_failures'] >= $res['max_failures']) {
|
||||
db()->query("UPDATE smtp_settings SET is_enabled = 0 WHERE id = 1");
|
||||
}
|
||||
} catch (\Exception $e) {}
|
||||
}
|
||||
|
||||
private static function resetFailures()
|
||||
{
|
||||
try {
|
||||
db()->query("UPDATE smtp_settings SET consecutive_failures = 0 WHERE id = 1");
|
||||
} catch (\Exception $e) {}
|
||||
}
|
||||
|
||||
// Send a contact message
|
||||
public static function sendContactMessage(string $name, string $email, string $message, $to = null, string $subject = 'New contact form')
|
||||
{
|
||||
// For simplicity, let's just use sendMail for everything
|
||||
$safeName = htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
$safeEmail = htmlspecialchars($email, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
$safeBody = nl2br(htmlspecialchars($message, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
|
||||
$html = "<p><strong>Name:</strong> {$safeName}</p><p><strong>Email:</strong> {$safeEmail}</p><hr>{$safeBody}";
|
||||
|
||||
return self::sendMail($to, $subject, $html, $message, ['reply_to' => $email]);
|
||||
}
|
||||
}
|
||||
71
m_services/config.php
Normal file
71
m_services/config.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
// Mail configuration sourced from environment variables or database.
|
||||
|
||||
if (!function_exists('env_val')) {
|
||||
function env_val(string $key, $default = null) {
|
||||
$v = getenv($key);
|
||||
return ($v === false || $v === null || $v === '') ? $default : $v;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if critical vars are missing from process env, try to parse executor/.env
|
||||
if (!function_exists('load_dotenv_if_needed')) {
|
||||
function load_dotenv_if_needed(array $keys): void {
|
||||
$missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === '');
|
||||
if (empty($missing)) return;
|
||||
static $loaded = false;
|
||||
if ($loaded) return;
|
||||
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
|
||||
if ($envPath && is_readable($envPath)) {
|
||||
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||
foreach ($lines as $line) {
|
||||
if ($line[0] === '#' || trim($line) === '') continue;
|
||||
if (!str_contains($line, '=')) continue;
|
||||
[$k, $v] = array_map('trim', explode('=', $line, 2));
|
||||
$v = trim($v, "' ");
|
||||
if ($k !== '' && (getenv($k) === false || getenv($k) === '')) {
|
||||
putenv("{$k}={$v}");
|
||||
}
|
||||
}
|
||||
$loaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
load_dotenv_if_needed([
|
||||
'MAIL_TRANSPORT','SMTP_HOST','SMTP_PORT','SMTP_SECURE','SMTP_USER','SMTP_PASS',
|
||||
'MAIL_FROM','MAIL_FROM_NAME','MAIL_REPLY_TO','MAIL_TO'
|
||||
]);
|
||||
|
||||
// Fetch from DB if available
|
||||
$db_settings = [];
|
||||
try {
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
$stmt = db()->query("SELECT * FROM smtp_settings WHERE id = 1");
|
||||
$db_settings = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
} catch (\Throwable $e) {
|
||||
// DB not ready or table missing
|
||||
}
|
||||
|
||||
$transport = env_val('MAIL_TRANSPORT', $db_settings['transport'] ?? 'smtp');
|
||||
$smtp_host = env_val('SMTP_HOST', $db_settings['smtp_host'] ?? '');
|
||||
$smtp_port = (int) env_val('SMTP_PORT', $db_settings['smtp_port'] ?? 587);
|
||||
$smtp_secure = env_val('SMTP_SECURE', $db_settings['smtp_secure'] ?? 'tls');
|
||||
$smtp_user = env_val('SMTP_USER', $db_settings['smtp_user'] ?? '');
|
||||
$smtp_pass = env_val('SMTP_PASS', $db_settings['smtp_pass'] ?? '');
|
||||
|
||||
$from_email = env_val('MAIL_FROM', $db_settings['from_email'] ?? 'no-reply@localhost');
|
||||
$from_name = env_val('MAIL_FROM_NAME', $db_settings['from_name'] ?? 'App');
|
||||
$reply_to = env_val('MAIL_REPLY_TO', $db_settings['reply_to'] ?? '');
|
||||
|
||||
return [
|
||||
'transport' => $transport,
|
||||
'smtp_host' => $smtp_host,
|
||||
'smtp_port' => $smtp_port,
|
||||
'smtp_secure' => $smtp_secure,
|
||||
'smtp_user' => $smtp_user,
|
||||
'smtp_pass' => $smtp_pass,
|
||||
'from_email' => $from_email,
|
||||
'from_name' => $from_name,
|
||||
'reply_to' => $reply_to,
|
||||
];
|
||||
7
m_services/index.php
Normal file
7
m_services/index.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
if (file_exists(__DIR__ . '/install.php')) {
|
||||
header('Location: install.php');
|
||||
} else {
|
||||
header('Location: ../index.php');
|
||||
}
|
||||
exit;
|
||||
123
m_services/install.php
Normal file
123
m_services/install.php
Normal file
@ -0,0 +1,123 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
$step = isset($_GET['step']) ? (int)$_GET['step'] : 1;
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
$configFile = __DIR__ . '/config.php';
|
||||
$envFile = __DIR__ . '/../.env';
|
||||
|
||||
if ($step === 2 && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$transport = $_POST['transport'] ?? 'smtp';
|
||||
$host = $_POST['host'] ?? '';
|
||||
$port = $_POST['port'] ?? '587';
|
||||
$secure = $_POST['secure'] ?? 'tls';
|
||||
$user = $_POST['user'] ?? '';
|
||||
$pass = $_POST['pass'] ?? '';
|
||||
$from = $_POST['from'] ?? '';
|
||||
$from_name = $_POST['from_name'] ?? '';
|
||||
|
||||
$envContent = "MAIL_TRANSPORT=$transport\n";
|
||||
$envContent .= "SMTP_HOST=$host\n";
|
||||
$envContent .= "SMTP_PORT=$port\n";
|
||||
$envContent .= "SMTP_SECURE=$secure\n";
|
||||
$envContent .= "SMTP_USER=$user\n";
|
||||
$envContent .= "SMTP_PASS=$pass\n";
|
||||
$envContent .= "MAIL_FROM=$from\n";
|
||||
$envContent .= "MAIL_FROM_NAME=$from_name\n";
|
||||
|
||||
if (file_put_contents($envFile, $envContent)) {
|
||||
header('Location: ' . $_SERVER['SCRIPT_NAME'] . '?step=3');
|
||||
exit;
|
||||
} else {
|
||||
$error = 'Failed to write .env file. Check permissions.';
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Mail Service Installation</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h3 class="mb-0">Mail Service Installation - Step <?= $step ?></h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger"><?= $error ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($step === 1): ?>
|
||||
<h5>Welcome to Mail Service Setup</h5>
|
||||
<p>This wizard will help you configure your SMTP settings.</p>
|
||||
<a href="<?= htmlspecialchars($_SERVER['SCRIPT_NAME']) ?>?step=2" class="btn btn-primary">Start Configuration</a>
|
||||
|
||||
<?php elseif ($step === 2): ?>
|
||||
<form method="POST" action="<?= htmlspecialchars($_SERVER['SCRIPT_NAME']) ?>?step=2">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Transport</label>
|
||||
<select name="transport" class="form-select">
|
||||
<option value="smtp">SMTP</option>
|
||||
<option value="sendmail">Sendmail</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">SMTP Host</label>
|
||||
<input type="text" name="host" class="form-control" required>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">SMTP Port</label>
|
||||
<input type="text" name="port" class="form-control" value="587" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Encryption</label>
|
||||
<select name="secure" class="form-select">
|
||||
<option value="tls">TLS</option>
|
||||
<option value="ssl">SSL</option>
|
||||
<option value="">None</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">SMTP Username</label>
|
||||
<input type="text" name="user" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">SMTP Password</label>
|
||||
<input type="password" name="pass" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">From Email</label>
|
||||
<input type="email" name="from" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">From Name</label>
|
||||
<input type="text" name="from_name" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save Settings</button>
|
||||
</form>
|
||||
|
||||
<?php elseif ($step === 3): ?>
|
||||
<div class="alert alert-success">
|
||||
Configuration saved successfully!
|
||||
</div>
|
||||
<p>The mail service is now ready to use.</p>
|
||||
<a href="../login.php" class="btn btn-success">Go to Login</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,235 +0,0 @@
|
||||
<?php
|
||||
// Minimal mail service for the workspace app (VM).
|
||||
// Usage:
|
||||
// require_once __DIR__ . '/MailService.php';
|
||||
// // Generic:
|
||||
// MailService::sendMail($to, $subject, $htmlBody, $textBody = null, $opts = []);
|
||||
// // Contact form helper:
|
||||
// MailService::sendContactMessage($name, $email, $message, $to = null, $subject = 'New contact form');
|
||||
|
||||
class MailService
|
||||
{
|
||||
// Universal mail sender (no attachments by design)
|
||||
public static function sendMail($to, string $subject, string $htmlBody, ?string $textBody = null, array $opts = [])
|
||||
{
|
||||
$cfg = self::loadConfig();
|
||||
|
||||
$autoload = __DIR__ . '/../vendor/autoload.php';
|
||||
if (file_exists($autoload)) {
|
||||
require_once $autoload;
|
||||
}
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
@require_once 'libphp-phpmailer/autoload.php';
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
@require_once 'libphp-phpmailer/src/Exception.php';
|
||||
@require_once 'libphp-phpmailer/src/SMTP.php';
|
||||
@require_once 'libphp-phpmailer/src/PHPMailer.php';
|
||||
}
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
@require_once 'PHPMailer/src/Exception.php';
|
||||
@require_once 'PHPMailer/src/SMTP.php';
|
||||
@require_once 'PHPMailer/src/PHPMailer.php';
|
||||
}
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
@require_once 'PHPMailer/Exception.php';
|
||||
@require_once 'PHPMailer/SMTP.php';
|
||||
@require_once 'PHPMailer/PHPMailer.php';
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
return [ 'success' => false, 'error' => 'PHPMailer not available' ];
|
||||
}
|
||||
|
||||
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
|
||||
try {
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $cfg['smtp_host'] ?? '';
|
||||
$mail->Port = (int)($cfg['smtp_port'] ?? 587);
|
||||
$secure = $cfg['smtp_secure'] ?? 'tls';
|
||||
if ($secure === 'ssl') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS;
|
||||
elseif ($secure === 'tls') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
|
||||
else $mail->SMTPSecure = false;
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $cfg['smtp_user'] ?? '';
|
||||
$mail->Password = $cfg['smtp_pass'] ?? '';
|
||||
|
||||
$fromEmail = $opts['from_email'] ?? ($cfg['from_email'] ?? 'no-reply@localhost');
|
||||
$fromName = $opts['from_name'] ?? ($cfg['from_name'] ?? 'App');
|
||||
$mail->setFrom($fromEmail, $fromName);
|
||||
if (!empty($opts['reply_to']) && filter_var($opts['reply_to'], FILTER_VALIDATE_EMAIL)) {
|
||||
$mail->addReplyTo($opts['reply_to']);
|
||||
} elseif (!empty($cfg['reply_to'])) {
|
||||
$mail->addReplyTo($cfg['reply_to']);
|
||||
}
|
||||
|
||||
// Recipients
|
||||
$toList = [];
|
||||
if ($to) {
|
||||
if (is_string($to)) $toList = array_map('trim', explode(',', $to));
|
||||
elseif (is_array($to)) $toList = $to;
|
||||
} elseif (!empty(getenv('MAIL_TO'))) {
|
||||
$toList = array_map('trim', explode(',', getenv('MAIL_TO')));
|
||||
}
|
||||
$added = 0;
|
||||
foreach ($toList as $addr) {
|
||||
if (filter_var($addr, FILTER_VALIDATE_EMAIL)) { $mail->addAddress($addr); $added++; }
|
||||
}
|
||||
if ($added === 0) {
|
||||
return [ 'success' => false, 'error' => 'No recipients defined (set MAIL_TO or pass $to)' ];
|
||||
}
|
||||
|
||||
foreach ((array)($opts['cc'] ?? []) as $cc) { if (filter_var($cc, FILTER_VALIDATE_EMAIL)) $mail->addCC($cc); }
|
||||
foreach ((array)($opts['bcc'] ?? []) as $bcc){ if (filter_var($bcc, FILTER_VALIDATE_EMAIL)) $mail->addBCC($bcc); }
|
||||
|
||||
// Optional DKIM
|
||||
if (!empty($cfg['dkim_domain']) && !empty($cfg['dkim_selector']) && !empty($cfg['dkim_private_key_path'])) {
|
||||
$mail->DKIM_domain = $cfg['dkim_domain'];
|
||||
$mail->DKIM_selector = $cfg['dkim_selector'];
|
||||
$mail->DKIM_private = $cfg['dkim_private_key_path'];
|
||||
}
|
||||
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $htmlBody;
|
||||
$mail->AltBody = $textBody ?? strip_tags($htmlBody);
|
||||
$ok = $mail->send();
|
||||
return [ 'success' => $ok ];
|
||||
} catch (\Throwable $e) {
|
||||
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ];
|
||||
}
|
||||
}
|
||||
private static function loadConfig(): array
|
||||
{
|
||||
$configPath = __DIR__ . '/config.php';
|
||||
if (!file_exists($configPath)) {
|
||||
throw new \RuntimeException('Mail config not found. Copy mail/config.sample.php to mail/config.php and fill in credentials.');
|
||||
}
|
||||
$cfg = require $configPath;
|
||||
if (!is_array($cfg)) {
|
||||
throw new \RuntimeException('Invalid mail config format: expected array');
|
||||
}
|
||||
return $cfg;
|
||||
}
|
||||
|
||||
// Send a contact message
|
||||
// $to can be: a single email string, a comma-separated list, an array of emails, or null (fallback to MAIL_TO/MAIL_FROM)
|
||||
public static function sendContactMessage(string $name, string $email, string $message, $to = null, string $subject = 'New contact form')
|
||||
{
|
||||
$cfg = self::loadConfig();
|
||||
|
||||
// Try Composer autoload if available (for PHPMailer)
|
||||
$autoload = __DIR__ . '/../vendor/autoload.php';
|
||||
if (file_exists($autoload)) {
|
||||
require_once $autoload;
|
||||
}
|
||||
// Fallback to system-wide PHPMailer (installed via apt: libphp-phpmailer)
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
// Debian/Ubuntu package layout (libphp-phpmailer)
|
||||
@require_once 'libphp-phpmailer/autoload.php';
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
@require_once 'libphp-phpmailer/src/Exception.php';
|
||||
@require_once 'libphp-phpmailer/src/SMTP.php';
|
||||
@require_once 'libphp-phpmailer/src/PHPMailer.php';
|
||||
}
|
||||
// Alternative layout (older PHPMailer package names)
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
@require_once 'PHPMailer/src/Exception.php';
|
||||
@require_once 'PHPMailer/src/SMTP.php';
|
||||
@require_once 'PHPMailer/src/PHPMailer.php';
|
||||
}
|
||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
@require_once 'PHPMailer/Exception.php';
|
||||
@require_once 'PHPMailer/SMTP.php';
|
||||
@require_once 'PHPMailer/PHPMailer.php';
|
||||
}
|
||||
}
|
||||
|
||||
$transport = $cfg['transport'] ?? 'smtp';
|
||||
if ($transport === 'smtp' && class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||
return self::sendViaPHPMailer($cfg, $name, $email, $message, $to, $subject);
|
||||
}
|
||||
|
||||
// Fallback: attempt native mail() — works only if MTA is configured on the VM
|
||||
return self::sendViaNativeMail($cfg, $name, $email, $message, $to, $subject);
|
||||
}
|
||||
|
||||
private static function sendViaPHPMailer(array $cfg, string $name, string $email, string $body, $to, string $subject)
|
||||
{
|
||||
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
|
||||
try {
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $cfg['smtp_host'] ?? '';
|
||||
$mail->Port = (int)($cfg['smtp_port'] ?? 587);
|
||||
$secure = $cfg['smtp_secure'] ?? 'tls';
|
||||
if ($secure === 'ssl') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS;
|
||||
elseif ($secure === 'tls') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
|
||||
else $mail->SMTPSecure = false;
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $cfg['smtp_user'] ?? '';
|
||||
$mail->Password = $cfg['smtp_pass'] ?? '';
|
||||
|
||||
$fromEmail = $cfg['from_email'] ?? 'no-reply@localhost';
|
||||
$fromName = $cfg['from_name'] ?? 'App';
|
||||
$mail->setFrom($fromEmail, $fromName);
|
||||
|
||||
// Use Reply-To for the user's email to avoid spoofing From
|
||||
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$mail->addReplyTo($email, $name ?: $email);
|
||||
}
|
||||
if (!empty($cfg['reply_to'])) {
|
||||
$mail->addReplyTo($cfg['reply_to']);
|
||||
}
|
||||
|
||||
// Destination: prefer dynamic recipients ($to), fallback to MAIL_TO; no silent FROM fallback
|
||||
$toList = [];
|
||||
if ($to) {
|
||||
if (is_string($to)) {
|
||||
// allow comma-separated list
|
||||
$toList = array_map('trim', explode(',', $to));
|
||||
} elseif (is_array($to)) {
|
||||
$toList = $to;
|
||||
}
|
||||
} elseif (!empty(getenv('MAIL_TO'))) {
|
||||
$toList = array_map('trim', explode(',', getenv('MAIL_TO')));
|
||||
}
|
||||
$added = 0;
|
||||
foreach ($toList as $addr) {
|
||||
if (filter_var($addr, FILTER_VALIDATE_EMAIL)) {
|
||||
$mail->addAddress($addr);
|
||||
$added++;
|
||||
}
|
||||
}
|
||||
if ($added === 0) {
|
||||
return [ 'success' => false, 'error' => 'No recipients defined (set MAIL_TO or pass $to)' ];
|
||||
}
|
||||
|
||||
// DKIM (optional)
|
||||
if (!empty($cfg['dkim_domain']) && !empty($cfg['dkim_selector']) && !empty($cfg['dkim_private_key_path'])) {
|
||||
$mail->DKIM_domain = $cfg['dkim_domain'];
|
||||
$mail->DKIM_selector = $cfg['dkim_selector'];
|
||||
$mail->DKIM_private = $cfg['dkim_private_key_path'];
|
||||
}
|
||||
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = $subject;
|
||||
$safeName = htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
$safeEmail = htmlspecialchars($email, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
$safeBody = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
|
||||
$mail->Body = "<p><strong>Name:</strong> {$safeName}</p><p><strong>Email:</strong> {$safeEmail}</p><hr>{$safeBody}";
|
||||
$mail->AltBody = "Name: {$name}\nEmail: {$email}\n\n{$body}";
|
||||
|
||||
$ok = $mail->send();
|
||||
return [ 'success' => $ok ];
|
||||
} catch (\Throwable $e) {
|
||||
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ];
|
||||
}
|
||||
}
|
||||
|
||||
private static function sendViaNativeMail(array $cfg, string $name, string $email, string $body, $to, string $subject)
|
||||
{
|
||||
$opts = ['reply_to' => $email];
|
||||
$html = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
|
||||
return self::sendMail($to, $subject, $html, $body, $opts);
|
||||
}
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
<?php
|
||||
// Mail configuration sourced from environment variables.
|
||||
// No secrets are stored here; the file just maps env -> config array for MailService.
|
||||
|
||||
function env_val(string $key, $default = null) {
|
||||
$v = getenv($key);
|
||||
return ($v === false || $v === null || $v === '') ? $default : $v;
|
||||
}
|
||||
|
||||
// Fallback: if critical vars are missing from process env, try to parse executor/.env
|
||||
// This helps in web/Apache contexts where .env is not exported.
|
||||
// Supports simple KEY=VALUE lines; ignores quotes and comments.
|
||||
function load_dotenv_if_needed(array $keys): void {
|
||||
$missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === '');
|
||||
if (empty($missing)) return;
|
||||
static $loaded = false;
|
||||
if ($loaded) return;
|
||||
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
|
||||
if ($envPath && is_readable($envPath)) {
|
||||
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||
foreach ($lines as $line) {
|
||||
if ($line[0] === '#' || trim($line) === '') continue;
|
||||
if (!str_contains($line, '=')) continue;
|
||||
[$k, $v] = array_map('trim', explode('=', $line, 2));
|
||||
// Strip potential surrounding quotes
|
||||
$v = trim($v, "\"' ");
|
||||
// Do not override existing env
|
||||
if ($k !== '' && (getenv($k) === false || getenv($k) === '')) {
|
||||
putenv("{$k}={$v}");
|
||||
}
|
||||
}
|
||||
$loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
load_dotenv_if_needed([
|
||||
'MAIL_TRANSPORT','SMTP_HOST','SMTP_PORT','SMTP_SECURE','SMTP_USER','SMTP_PASS',
|
||||
'MAIL_FROM','MAIL_FROM_NAME','MAIL_REPLY_TO','MAIL_TO',
|
||||
'DKIM_DOMAIN','DKIM_SELECTOR','DKIM_PRIVATE_KEY_PATH'
|
||||
]);
|
||||
|
||||
$transport = env_val('MAIL_TRANSPORT', 'smtp');
|
||||
$smtp_host = env_val('SMTP_HOST');
|
||||
$smtp_port = (int) env_val('SMTP_PORT', 587);
|
||||
$smtp_secure = env_val('SMTP_SECURE', 'tls'); // tls | ssl | null
|
||||
$smtp_user = env_val('SMTP_USER');
|
||||
$smtp_pass = env_val('SMTP_PASS');
|
||||
|
||||
$from_email = env_val('MAIL_FROM', 'no-reply@localhost');
|
||||
$from_name = env_val('MAIL_FROM_NAME', 'App');
|
||||
$reply_to = env_val('MAIL_REPLY_TO');
|
||||
|
||||
$dkim_domain = env_val('DKIM_DOMAIN');
|
||||
$dkim_selector = env_val('DKIM_SELECTOR');
|
||||
$dkim_private_key_path = env_val('DKIM_PRIVATE_KEY_PATH');
|
||||
|
||||
return [
|
||||
'transport' => $transport,
|
||||
|
||||
// SMTP
|
||||
'smtp_host' => $smtp_host,
|
||||
'smtp_port' => $smtp_port,
|
||||
'smtp_secure' => $smtp_secure,
|
||||
'smtp_user' => $smtp_user,
|
||||
'smtp_pass' => $smtp_pass,
|
||||
|
||||
// From / Reply-To
|
||||
'from_email' => $from_email,
|
||||
'from_name' => $from_name,
|
||||
'reply_to' => $reply_to,
|
||||
|
||||
// DKIM (optional)
|
||||
'dkim_domain' => $dkim_domain,
|
||||
'dkim_selector' => $dkim_selector,
|
||||
'dkim_private_key_path' => $dkim_private_key_path,
|
||||
];
|
||||
461
meetings.php
Normal file
461
meetings.php
Normal file
@ -0,0 +1,461 @@
|
||||
<?php
|
||||
ob_start();
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
require_once __DIR__ . '/includes/pagination.php';
|
||||
|
||||
if (!canView('meetings')) {
|
||||
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('meetings')) || ($action === 'edit' && !canEdit('meetings'))) {
|
||||
$error = 'ليس لديك صلاحية للقيام بهذا الإجراء';
|
||||
} else {
|
||||
$title = $_POST['title'] ?? '';
|
||||
$description = $_POST['description'] ?? '';
|
||||
$agenda = $_POST['agenda'] ?? '';
|
||||
$attendees = $_POST['attendees'] ?? '';
|
||||
$absentees = $_POST['absentees'] ?? '';
|
||||
$meeting_details = $_POST['meeting_details'] ?? '';
|
||||
$start_time = $_POST['start_time'] ?? '';
|
||||
$end_time = $_POST['end_time'] ?? '';
|
||||
$location = $_POST['location'] ?? '';
|
||||
$status = $_POST['status'] ?? 'scheduled';
|
||||
|
||||
try {
|
||||
$db = db();
|
||||
|
||||
if ($action === 'add') {
|
||||
$stmt = $db->prepare("INSERT INTO meetings (title, description, agenda, attendees, absentees, meeting_details, start_time, end_time, location, status, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$title, $description, $agenda, $attendees, $absentees, $meeting_details, $start_time, $end_time, $location, $status, $_SESSION['user_id']]);
|
||||
$_SESSION['success'] = 'تم جدولة الاجتماع بنجاح';
|
||||
} else {
|
||||
$stmt = $db->prepare("UPDATE meetings SET title=?, description=?, agenda=?, attendees=?, absentees=?, meeting_details=?, start_time=?, end_time=?, location=?, status=? WHERE id=?");
|
||||
$stmt->execute([$title, $description, $agenda, $attendees, $absentees, $meeting_details, $start_time, $end_time, $location, $status, $id]);
|
||||
$_SESSION['success'] = 'تم تحديث الاجتماع بنجاح';
|
||||
}
|
||||
redirect('meetings.php');
|
||||
} catch (PDOException $e) {
|
||||
$error = 'حدث خطأ: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET['action']) && $_GET['action'] === 'delete' && isset($_GET['id'])) {
|
||||
if (!canDelete('meetings')) redirect('meetings.php');
|
||||
$id = $_GET['id'];
|
||||
$db = db();
|
||||
|
||||
$stmt = $db->prepare("DELETE FROM meetings WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
$_SESSION['success'] = 'تم حذف الاجتماع بنجاح';
|
||||
redirect('meetings.php');
|
||||
}
|
||||
|
||||
// Fetch Data for List
|
||||
$date_from = $_GET['date_from'] ?? date('Y-m-01');
|
||||
$date_to = $_GET['date_to'] ?? date('Y-m-t');
|
||||
$status_filter = $_GET['status'] ?? '';
|
||||
$search = $_GET['search'] ?? '';
|
||||
|
||||
// Base WHERE conditions
|
||||
$whereConditions = ["DATE(m.start_time) BETWEEN ? AND ?"];
|
||||
$params = [$date_from, $date_to];
|
||||
|
||||
if ($status_filter) {
|
||||
$whereConditions[] = "m.status = ?";
|
||||
$params[] = $status_filter;
|
||||
}
|
||||
if ($search) {
|
||||
$whereConditions[] = "(m.title LIKE ? OR m.description LIKE ? OR m.location LIKE ?)";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
}
|
||||
|
||||
$whereClause = implode(' AND ', $whereConditions);
|
||||
|
||||
// Pagination
|
||||
$page = $_GET['page'] ?? 1;
|
||||
$perPage = 10;
|
||||
|
||||
// Count Total Items
|
||||
$countSql = "SELECT COUNT(*) FROM meetings m WHERE $whereClause";
|
||||
$countStmt = db()->prepare($countSql);
|
||||
$countStmt->execute($params);
|
||||
$totalMeetings = $countStmt->fetchColumn();
|
||||
|
||||
$pagination = getPagination($page, $totalMeetings, $perPage);
|
||||
|
||||
// Fetch Items with Limit
|
||||
$sql = "SELECT m.*, u.username as created_by_name
|
||||
FROM meetings m
|
||||
LEFT JOIN users u ON m.created_by = u.id
|
||||
WHERE $whereClause
|
||||
ORDER BY m.start_time ASC
|
||||
LIMIT ? OFFSET ?";
|
||||
|
||||
// Add LIMIT and OFFSET to params
|
||||
$params[] = $pagination['limit'];
|
||||
$params[] = $pagination['offset'];
|
||||
|
||||
$stmt = db()->prepare($sql);
|
||||
|
||||
// Bind params manually because limit/offset must be integers
|
||||
// But wait, $params is mixed string/int.
|
||||
// PDO::execute($params) treats all as strings which is fine for limit/offset in MySQL usually,
|
||||
// but strictly speaking, LIMIT/OFFSET should be ints.
|
||||
// Let's bind all params.
|
||||
foreach ($params as $k => $v) {
|
||||
// 1-based index
|
||||
$type = is_int($v) ? PDO::PARAM_INT : PDO::PARAM_STR;
|
||||
$stmt->bindValue($k + 1, $v, $type);
|
||||
}
|
||||
$stmt->execute();
|
||||
$meetings = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (isset($_SESSION['success'])) {
|
||||
$success = $_SESSION['success'];
|
||||
unset($_SESSION['success']);
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- Summernote Lite CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.css" rel="stylesheet">
|
||||
<style>
|
||||
/* Custom Styles for Summernote Integration */
|
||||
.note-editor .note-toolbar {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
.note-editable {
|
||||
background: #fff;
|
||||
min-height: 200px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<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('meetings')): ?>
|
||||
<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="status" class="form-select">
|
||||
<option value="">الكل</option>
|
||||
<option value="scheduled" <?= $status_filter == 'scheduled' ? 'selected' : '' ?>>مجدول</option>
|
||||
<option value="completed" <?= $status_filter == 'completed' ? 'selected' : '' ?>>منتهي</option>
|
||||
<option value="cancelled" <?= $status_filter == 'cancelled' ? 'selected' : '' ?>>ملغي</option>
|
||||
</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 class="text-center">الإجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($meetings)): ?>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-5 text-muted">لا توجد اجتماعات مطابقة</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($meetings as $meeting): ?>
|
||||
<?php
|
||||
$status_class = match($meeting['status']) {
|
||||
'scheduled' => 'bg-primary',
|
||||
'completed' => 'bg-success',
|
||||
'cancelled' => 'bg-danger',
|
||||
default => 'bg-secondary'
|
||||
};
|
||||
$status_text = match($meeting['status']) {
|
||||
'scheduled' => 'مجدول',
|
||||
'completed' => 'منتهي',
|
||||
'cancelled' => 'ملغي',
|
||||
default => $meeting['status']
|
||||
};
|
||||
// Strip tags for preview, but keep it clean
|
||||
$agenda_preview = strip_tags($meeting['agenda'] ?? '');
|
||||
?>
|
||||
<tr>
|
||||
<td class="ps-4 fw-bold">
|
||||
<?= htmlspecialchars($meeting['title']) ?>
|
||||
<?php if ($agenda_preview): ?>
|
||||
<div class="small text-muted fw-normal text-truncate" style="max-width: 250px;"><?= htmlspecialchars($agenda_preview) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<div><i class="fas fa-calendar-alt text-muted me-1"></i> <?= date('Y-m-d', strtotime($meeting['start_time'])) ?></div>
|
||||
<div class="small text-muted"><i class="fas fa-clock me-1"></i> <?= date('H:i', strtotime($meeting['start_time'])) ?> - <?= date('H:i', strtotime($meeting['end_time'])) ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($meeting['location']): ?>
|
||||
<i class="fas fa-map-marker-alt text-danger me-1"></i> <?= htmlspecialchars($meeting['location']) ?>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($meeting['created_by_name']) ?></td>
|
||||
<td><span class="badge <?= $status_class ?>"><?= $status_text ?></span></td>
|
||||
<td class="text-center">
|
||||
<?php if (canEdit('meetings')): ?>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick='openModal("edit", <?= json_encode($meeting, JSON_HEX_APOS | JSON_HEX_QUOT) ?>)'>
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<a href="print_meeting.php?id=<?= $meeting['id'] ?>" target="_blank" class="btn btn-sm btn-outline-secondary" title="طباعة">
|
||||
<i class="fas fa-print"></i>
|
||||
</a>
|
||||
<?php if (canDelete('meetings')): ?>
|
||||
<a href="javascript:void(0)" onclick="confirmDelete(<?= $meeting['id'] ?>)" class="btn btn-sm btn-outline-danger">
|
||||
<i class="fas fa-trash"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-white">
|
||||
<?= renderPagination($pagination['current_page'], $pagination['total_pages']) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="meetingModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<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" id="meetingForm">
|
||||
<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="title" id="modalTitleInput" class="form-control" required placeholder="مثال: اجتماع الفريق الأسبوعي">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">وقت البدء</label>
|
||||
<input type="datetime-local" name="start_time" id="modalStartTime" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">وقت الانتهاء</label>
|
||||
<input type="datetime-local" name="end_time" id="modalEndTime" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">الموقع / الرابط</label>
|
||||
<input type="text" name="location" id="modalLocation" class="form-control" placeholder="غرفة الاجتماعات، Zoom، Google Meet...">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">جدول الأعمال</label>
|
||||
<textarea name="agenda" id="agenda" class="summernote" placeholder="أدخل بنود جدول الأعمال والمواضيع المطروحة للنقاش هنا..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">الحضور</label>
|
||||
<textarea name="attendees" id="modalAttendees" class="form-control" rows="2" placeholder="أسماء الحضور..."></textarea>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">الغياب</label>
|
||||
<textarea name="absentees" id="modalAbsentees" class="form-control" rows="2" placeholder="أسماء الغائبين..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">تفاصيل الاجتماع / المحضر</label>
|
||||
<textarea name="meeting_details" id="meeting_details" class="summernote" placeholder="أدخل تفاصيل محضر الاجتماع، القرارات المتخذة، والنقاط التي تمت مناقشتها..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold"> القرارات والإلتزامات</label>
|
||||
<textarea name="description" id="description" class="summernote" placeholder="القرارات والإلتزامات..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">الحالة</label>
|
||||
<select name="status" id="modalStatus" class="form-select">
|
||||
<option value="scheduled">مجدول</option>
|
||||
<option value="completed">منتهي</option>
|
||||
<option value="cancelled">ملغي</option>
|
||||
</select>
|
||||
</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>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
|
||||
<!-- jQuery (required for Summernote) -->
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
|
||||
<!-- Summernote Lite JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize Summernote
|
||||
$('.summernote').each(function() {
|
||||
$(this).summernote({
|
||||
placeholder: $(this).attr('placeholder') || 'أدخل التفاصيل هنا...',
|
||||
tabsize: 2,
|
||||
height: 200,
|
||||
toolbar: [
|
||||
['style', ['style']],
|
||||
['font', ['bold', 'underline', 'clear', 'strikethrough', 'superscript', 'subscript']],
|
||||
['fontname', ['fontname']],
|
||||
['color', ['color']],
|
||||
['para', ['ul', 'ol', 'paragraph']],
|
||||
['table', ['table']],
|
||||
['insert', ['link', 'picture', 'video', 'hr']],
|
||||
['view', ['fullscreen', 'codeview', 'help']]
|
||||
],
|
||||
// Ensure RTL support if needed (Summernote usually auto-detects or needs plugin, but 'dir=rtl' on body often helps)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let meetingModal;
|
||||
|
||||
function openModal(action, data = null) {
|
||||
if (!meetingModal) {
|
||||
meetingModal = new bootstrap.Modal(document.getElementById('meetingModal'));
|
||||
}
|
||||
|
||||
document.getElementById('modalAction').value = action;
|
||||
const title = document.getElementById('modalTitle');
|
||||
|
||||
// Reset Summernote content first
|
||||
$('.summernote').summernote('code', '');
|
||||
|
||||
if (action === 'add') {
|
||||
title.textContent = 'جدولة اجتماع جديد';
|
||||
document.getElementById('modalId').value = 0;
|
||||
document.getElementById('modalTitleInput').value = '';
|
||||
document.getElementById('modalAttendees').value = '';
|
||||
document.getElementById('modalAbsentees').value = '';
|
||||
document.getElementById('modalLocation').value = '';
|
||||
|
||||
// Default start time: Next hour, :00
|
||||
const now = new Date();
|
||||
now.setMinutes(0, 0, 0);
|
||||
now.setHours(now.getHours() + 1);
|
||||
|
||||
const formatDateTime = (date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
document.getElementById('modalStartTime').value = formatDateTime(now);
|
||||
|
||||
const end = new Date(now);
|
||||
end.setHours(end.getHours() + 1);
|
||||
document.getElementById('modalEndTime').value = formatDateTime(end);
|
||||
|
||||
document.getElementById('modalStatus').value = 'scheduled';
|
||||
} else {
|
||||
title.textContent = 'تعديل الاجتماع';
|
||||
document.getElementById('modalId').value = data.id;
|
||||
document.getElementById('modalTitleInput').value = data.title;
|
||||
document.getElementById('modalAttendees').value = data.attendees || '';
|
||||
document.getElementById('modalAbsentees').value = data.absentees || '';
|
||||
document.getElementById('modalLocation').value = data.location || '';
|
||||
document.getElementById('modalStartTime').value = data.start_time.replace(' ', 'T').slice(0, 16);
|
||||
document.getElementById('modalEndTime').value = data.end_time.replace(' ', 'T').slice(0, 16);
|
||||
document.getElementById('modalStatus').value = data.status;
|
||||
|
||||
// Set Summernote content
|
||||
$('#agenda').summernote('code', data.agenda || '');
|
||||
$('#meeting_details').summernote('code', data.meeting_details || '');
|
||||
$('#description').summernote('code', data.description || '');
|
||||
}
|
||||
|
||||
meetingModal.show();
|
||||
}
|
||||
|
||||
function confirmDelete(id) {
|
||||
if (confirm('هل أنت متأكد من حذف هذا الاجتماع؟')) {
|
||||
window.location.href = 'meetings.php?action=delete&id=' + id;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
538
outbound.php
Normal file
538
outbound.php
Normal file
@ -0,0 +1,538 @@
|
||||
<?php
|
||||
require_once 'includes/header.php';
|
||||
require_once 'includes/pagination.php';
|
||||
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
// Handle CRUD operations
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
if (!canEdit('outbound') && !canAdd('outbound')) {
|
||||
$error = 'ليس لديك صلاحية للقيام بهذا الإجراء.';
|
||||
} else {
|
||||
$action = $_POST['action'];
|
||||
$id = $_POST['id'] ?? 0;
|
||||
$ref_no = $_POST['ref_no'] ?? '';
|
||||
$date_registered = $_POST['date_registered'] ?? date('Y-m-d');
|
||||
$due_date = !empty($_POST['due_date']) ? $_POST['due_date'] : null;
|
||||
$sender = $_POST['sender'] ?? '';
|
||||
$recipient = $_POST['recipient'] ?? '';
|
||||
$subject = $_POST['subject'] ?? '';
|
||||
$description = $_POST['description'] ?? '';
|
||||
$status_id = !empty($_POST['status_id']) ? $_POST['status_id'] : null;
|
||||
$assigned_to = !empty($_POST['assigned_to']) ? $_POST['assigned_to'] : null;
|
||||
|
||||
if ($action === 'add' || $action === 'edit') {
|
||||
try {
|
||||
db()->beginTransaction();
|
||||
|
||||
if ($action === 'add') {
|
||||
if (!canAdd('outbound')) throw new Exception('ليس لديك صلاحية الإضافة.');
|
||||
$stmt = db()->prepare("INSERT INTO outbound_mail (ref_no, date_registered, due_date, sender, recipient, subject, description, status_id, assigned_to, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$ref_no, $date_registered, $due_date, $sender, $recipient, $subject, $description, $status_id, $assigned_to, $_SESSION['user_id']]);
|
||||
$id = db()->lastInsertId();
|
||||
$success = 'تم إضافة البريد الصادر بنجاح.';
|
||||
} else {
|
||||
if (!canEdit('outbound')) throw new Exception('ليس لديك صلاحية التعديل.');
|
||||
$stmt = db()->prepare("UPDATE outbound_mail SET ref_no = ?, date_registered = ?, due_date = ?, sender = ?, recipient = ?, subject = ?, description = ?, status_id = ?, assigned_to = ? WHERE id = ?");
|
||||
$stmt->execute([$ref_no, $date_registered, $due_date, $sender, $recipient, $subject, $description, $status_id, $assigned_to, $id]);
|
||||
$success = 'تم تحديث بيانات البريد الصادر بنجاح.';
|
||||
}
|
||||
|
||||
// Handle file uploads
|
||||
if (isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0])) {
|
||||
$upload_dir = 'uploads/attachments/';
|
||||
if (!is_dir($upload_dir)) mkdir($upload_dir, 0777, true);
|
||||
|
||||
for ($i = 0; $i < count($_FILES['attachments']['name']); $i++) {
|
||||
if ($_FILES['attachments']['error'][$i] === 0) {
|
||||
$filename = time() . '_' . $_FILES['attachments']['name'][$i];
|
||||
$filepath = $upload_dir . $filename;
|
||||
if (move_uploaded_file($_FILES['attachments']['tmp_name'][$i], $filepath)) {
|
||||
$stmt = db()->prepare("INSERT INTO outbound_attachments (mail_id, display_name, file_path, file_name, file_size) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$id, $_FILES['attachments']['name'][$i], $filepath, $_FILES['attachments']['name'][$i], $_FILES['attachments']['size'][$i]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db()->commit();
|
||||
} catch (Exception $e) {
|
||||
db()->rollBack();
|
||||
$error = 'خطأ: ' . $e->getMessage();
|
||||
}
|
||||
} elseif ($action === 'delete') {
|
||||
if (!canDelete('outbound')) {
|
||||
$error = 'ليس لديك صلاحية الحذف.';
|
||||
} else {
|
||||
$stmt = db()->prepare("DELETE FROM outbound_mail WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$success = 'تم حذف البريد الصادر بنجاح.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch stats
|
||||
$total_stmt = db()->query("SELECT COUNT(*) FROM outbound_mail");
|
||||
$total_outbound = $total_stmt->fetchColumn();
|
||||
|
||||
$completed_stmt = db()->prepare("SELECT COUNT(*) FROM outbound_mail WHERE status_id IN (SELECT id FROM mailbox_statuses WHERE name LIKE '%مكتمل%' OR name LIKE '%منتهي%')");
|
||||
$completed_stmt->execute();
|
||||
$completed_outbound = $completed_stmt->fetchColumn();
|
||||
|
||||
// Search and Filter
|
||||
$where = "WHERE 1=1";
|
||||
$params = [];
|
||||
|
||||
if (isset($_GET['search']) && !empty($_GET['search'])) {
|
||||
$where .= " AND (m.ref_no LIKE ? OR m.subject LIKE ? OR m.sender LIKE ? OR m.recipient LIKE ?)";
|
||||
$search = "%" . $_GET['search'] . "%";
|
||||
$params = array_merge($params, [$search, $search, $search, $search]);
|
||||
}
|
||||
|
||||
if (isset($_GET['status_id']) && !empty($_GET['status_id'])) {
|
||||
$where .= " AND m.status_id = ?";
|
||||
$params[] = $_GET['status_id'];
|
||||
}
|
||||
|
||||
// Pagination
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
if ($page < 1) $page = 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
// Count filtered
|
||||
$countQuery = "SELECT COUNT(*) FROM outbound_mail m $where";
|
||||
$countStmt = db()->prepare($countQuery);
|
||||
$countStmt->execute($params);
|
||||
$totalFiltered = $countStmt->fetchColumn();
|
||||
|
||||
|
||||
$query = "SELECT m.*, s.name as status_name, s.color as status_color, u.full_name as assigned_to_name,
|
||||
(SELECT GROUP_CONCAT(display_name SEPARATOR '|||') FROM outbound_attachments WHERE mail_id = m.id) as attachment_names
|
||||
FROM outbound_mail m
|
||||
LEFT JOIN mailbox_statuses s ON m.status_id = s.id
|
||||
LEFT JOIN users u ON m.assigned_to = u.id
|
||||
$where
|
||||
ORDER BY m.date_registered DESC, m.id DESC
|
||||
LIMIT $limit OFFSET $offset";
|
||||
|
||||
$stmt = db()->prepare($query);
|
||||
$stmt->execute($params);
|
||||
$mails = $stmt->fetchAll();
|
||||
|
||||
$statuses = db()->query("SELECT * FROM mailbox_statuses ORDER BY id ASC")->fetchAll();
|
||||
$users = db()->query("SELECT id, full_name, username FROM users ORDER BY full_name ASC")->fetchAll();
|
||||
$default_status_id = db()->query("SELECT id FROM mailbox_statuses WHERE is_default = 1 LIMIT 1")->fetchColumn() ?: ($statuses[0]['id'] ?? null);
|
||||
|
||||
$deepLinkData = null;
|
||||
if (isset($_GET['id'])) {
|
||||
$dlStmt = db()->prepare("SELECT m.*, (SELECT GROUP_CONCAT(display_name SEPARATOR '|||') FROM outbound_attachments WHERE mail_id = m.id) as attachment_names FROM outbound_mail m WHERE m.id = ?");
|
||||
$dlStmt->execute([$_GET['id']]);
|
||||
$deepLinkData = $dlStmt->fetch();
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<h2 class="fw-bold mb-1"><i class="fas fa-upload text-primary me-2"></i> البريد الصادر</h2>
|
||||
<p class="text-muted">إدارة المراسلات الصادرة إلى الجهات الخارجية وتتبع حالاتها.</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-md-end d-flex align-items-center justify-content-md-end gap-2">
|
||||
<?php if (canAdd('outbound')): ?>
|
||||
<button class="btn btn-primary px-4 py-2" onclick="openMailModal('add')">
|
||||
<i class="fas fa-plus me-1"></i> إضافة صادر جديد
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle bg-primary bg-opacity-10 p-3 me-3">
|
||||
<i class="fas fa-paper-plane text-primary fa-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="card-subtitle text-muted mb-1 small">إجمالي الصادر</h6>
|
||||
<h3 class="card-title mb-0 fw-bold"><?= $total_outbound ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle bg-success bg-opacity-10 p-3 me-3">
|
||||
<i class="fas fa-check-circle text-success fa-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="card-subtitle text-muted mb-1 small">مكتمل / مرسل</h6>
|
||||
<h3 class="card-title mb-0 fw-bold"><?= $completed_outbound ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger shadow-sm border-0 mb-4"><?= $error ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success shadow-sm border-0 mb-4"><?= $success ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-3">
|
||||
<form method="GET" class="row g-2 align-items-center">
|
||||
<div class="col-md-5">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-white border-end-0"><i class="fas fa-search text-muted"></i></span>
|
||||
<input type="text" name="search" class="form-control border-start-0" placeholder="بحث برقم القيد، الموضوع، أو الجهة..." value="<?= htmlspecialchars($_GET['search'] ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<select name="status_id" class="form-select" onchange="this.form.submit()">
|
||||
<option value="">جميع الحالات</option>
|
||||
<?php foreach ($statuses as $status): ?>
|
||||
<option value="<?= $status['id'] ?>" <?= (isset($_GET['status_id']) && $_GET['status_id'] == $status['id']) ? 'selected' : '' ?>><?= $status['name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 text-end">
|
||||
<button type="submit" class="btn btn-light w-100">تصفية</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mails Table -->
|
||||
<div class="card border-0 shadow-sm overflow-hidden">
|
||||
<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 class="text-center pe-4">الإجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($mails)): ?>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-5 text-muted">
|
||||
<i class="fas fa-paper-plane fa-3x mb-3 opacity-20"></i>
|
||||
<p>لا يوجد بريد صادر حالياً.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($mails as $mail): ?>
|
||||
<tr>
|
||||
<td class="ps-4"><span class="fw-bold text-primary"><?= htmlspecialchars($mail['ref_no']) ?></span></td>
|
||||
<td><?= date('Y-m-d', strtotime($mail['date_registered'])) ?></td>
|
||||
<td>
|
||||
<div class="fw-semibold text-truncate" style="max-width: 300px;"><?= htmlspecialchars($mail['subject']) ?></div>
|
||||
<?php if ($mail['attachment_names']): ?>
|
||||
<span class="badge bg-light text-muted fw-normal" style="font-size: 0.65rem;">
|
||||
<i class="fas fa-paperclip me-1"></i> <?= count(explode('|||', $mail['attachment_names'])) ?> مرفقات
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($mail['recipient']) ?></td>
|
||||
<td>
|
||||
<span class="badge rounded-pill" style="background-color: <?= $mail['status_color'] ?>20; color: <?= $mail['status_color'] ?>;">
|
||||
<i class="fas fa-circle me-1 small"></i> <?= htmlspecialchars($mail['status_name']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center pe-4">
|
||||
<div class="btn-group shadow-sm rounded">
|
||||
<a href="view_mail.php?id=<?= $mail['id'] ?>&type=outbound" class="btn btn-sm btn-white text-primary border" title="عرض">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="print_outbound.php?id=<?= $mail['id'] ?>" target="_blank" class="btn btn-sm btn-white text-secondary border" title="طباعة">
|
||||
<i class="fas fa-print"></i>
|
||||
</a>
|
||||
<?php if (canEdit('outbound')): ?>
|
||||
<button class="btn btn-sm btn-white text-warning border" onclick='openMailModal("edit", <?= json_encode($mail) ?>)' title="تعديل">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if (canDelete('outbound')): ?>
|
||||
<form method="POST" class="d-inline" onsubmit="return confirm('هل أنت متأكد من حذف هذا البريد؟');">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="id" value="<?= $mail['id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-white text-danger border rounded-0" title="حذف">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php renderPagination($page, $totalFiltered, $limit); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Modal -->
|
||||
<div class="modal fade" id="mailModal" tabindex="-1" aria-labelledby="mailModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header bg-primary text-white py-3">
|
||||
<h5 class="modal-title fw-bold" id="mailModalLabel">بريد صادر جديد</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="mailForm" method="POST" enctype="multipart/form-data">
|
||||
<div class="modal-body p-4">
|
||||
<input type="hidden" name="action" id="modalAction" value="add">
|
||||
<input type="hidden" name="id" id="modalId" value="0">
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">رقم القيد</label>
|
||||
<input type="text" name="ref_no" id="modalRefNo" class="form-control" readonly required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">تاريخ التسجيل</label>
|
||||
<input type="date" name="date_registered" id="modalDateRegistered" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">الجهة الصادر منها (الداخلية)</label>
|
||||
<input type="text" name="sender" id="modalSender" class="form-control" required placeholder="مثال: الشؤون القانونية">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">الجهة المستلمة (الخارجية)</label>
|
||||
<input type="text" name="recipient" id="modalRecipient" class="form-control" required placeholder="مثال: البنك المركزي">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label small fw-bold">الموضوع</label>
|
||||
<input type="text" name="subject" id="modalSubject" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label small fw-bold">محتوى الخطاب / الوصف</label>
|
||||
<textarea name="description" id="description_editor" class="form-control" rows="5"></textarea>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">تاريخ الاستحقاق (اختياري)</label>
|
||||
<input type="date" name="due_date" id="modalDueDate" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">الحالة</label>
|
||||
<select name="status_id" id="modalStatusId" class="form-select" required>
|
||||
<?php foreach ($statuses as $status): ?>
|
||||
<option value="<?= $status['id'] ?>"><?= $status['name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label small fw-bold">إسناد للمتابعة (اختياري)</label>
|
||||
<select name="assigned_to" id="modalAssignedTo" class="form-select">
|
||||
<option value="">--- اختر مستخدم ---</option>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<option value="<?= $user['id'] ?>"><?= htmlspecialchars($user['full_name'] ?: $user['username']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-12 mt-4">
|
||||
<div class="bg-light p-3 rounded">
|
||||
<label class="form-label small fw-bold mb-2 d-block">المرفقات</label>
|
||||
<input type="file" name="attachments[]" id="modalAttachmentsInput" class="form-control" multiple>
|
||||
<div id="modalExistingAttachments" class="mt-2"></div>
|
||||
<div id="modalSelectedAttachments" class="mt-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light border-0">
|
||||
<button type="button" class="btn btn-white border px-4" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary px-4">حفظ الصادر</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Custom Table Styles */
|
||||
.table thead th {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05rem;
|
||||
font-weight: 700;
|
||||
color: #6c757d;
|
||||
border-top: none;
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
.btn-white {
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
}
|
||||
.btn-white:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.badge {
|
||||
padding: 0.5em 0.8em;
|
||||
font-weight: 500;
|
||||
}
|
||||
.input-group-text {
|
||||
color: #adb5bd;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let mailModal;
|
||||
|
||||
function initEditors() {
|
||||
if (typeof tinymce === 'undefined') {
|
||||
console.error('TinyMCE not loaded');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return tinymce.init({
|
||||
selector: '#description_editor',
|
||||
language: 'ar', language_url: 'https://cdn.jsdelivr.net/npm/tinymce-i18n@23.10.9/langs6/ar.js',
|
||||
directionality: 'rtl',
|
||||
height: 300,
|
||||
plugins: 'advlist autolink lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table help wordcount',
|
||||
toolbar: 'undo redo | fontfamily fontsize | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | help',
|
||||
font_size_formats: '8pt 10pt 12pt 14pt 18pt 24pt 36pt',
|
||||
promotion: false,
|
||||
branding: false
|
||||
});
|
||||
}
|
||||
|
||||
function openMailModal(action, data = null) {
|
||||
if (!mailModal) {
|
||||
const modalEl = document.getElementById('mailModal');
|
||||
if (typeof bootstrap !== 'undefined') {
|
||||
mailModal = new bootstrap.Modal(modalEl);
|
||||
} else {
|
||||
console.error('Bootstrap not loaded');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const label = document.getElementById('mailModalLabel');
|
||||
const modalAction = document.getElementById('modalAction');
|
||||
const modalId = document.getElementById('modalId');
|
||||
const existingAttachmentsDiv = document.getElementById('modalExistingAttachments');
|
||||
const selectedAttachmentsDiv = document.getElementById('modalSelectedAttachments');
|
||||
const attachmentsInput = document.getElementById('modalAttachmentsInput');
|
||||
|
||||
const fields = {
|
||||
ref_no: document.getElementById('modalRefNo'),
|
||||
date_registered: document.getElementById('modalDateRegistered'),
|
||||
due_date: document.getElementById('modalDueDate'),
|
||||
sender: document.getElementById('modalSender'),
|
||||
recipient: document.getElementById('modalRecipient'),
|
||||
subject: document.getElementById('modalSubject'),
|
||||
status_id: document.getElementById('modalStatusId'),
|
||||
assigned_to: document.getElementById('modalAssignedTo')
|
||||
};
|
||||
|
||||
modalAction.value = action;
|
||||
existingAttachmentsDiv.innerHTML = '';
|
||||
selectedAttachmentsDiv.innerHTML = '';
|
||||
if (attachmentsInput) attachmentsInput.value = '';
|
||||
|
||||
if (action === 'add') {
|
||||
label.textContent = 'إضافة بريد صادر جديد';
|
||||
modalId.value = '0';
|
||||
Object.keys(fields).forEach(key => {
|
||||
if (fields[key]) {
|
||||
if (key === 'date_registered') fields[key].value = '<?= date('Y-m-d') ?>';
|
||||
else if (key === 'status_id') fields[key].value = '<?= $default_status_id ?>';
|
||||
else if (key === 'ref_no') fields[key].value = '<?= generateRefNo('outbound') ?>';
|
||||
else fields[key].value = '';
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof tinymce !== 'undefined' && tinymce.get('description_editor')) {
|
||||
tinymce.get('description_editor').setContent('');
|
||||
} else {
|
||||
document.getElementById('description_editor').value = '';
|
||||
}
|
||||
} else {
|
||||
label.textContent = 'تعديل البريد الصادر';
|
||||
modalId.value = data.id;
|
||||
Object.keys(fields).forEach(key => {
|
||||
if (fields[key]) fields[key].value = data[key] || '';
|
||||
});
|
||||
|
||||
if (typeof tinymce !== 'undefined' && tinymce.get('description_editor')) {
|
||||
tinymce.get('description_editor').setContent(data.description || '');
|
||||
} else {
|
||||
document.getElementById('description_editor').value = data.description || '';
|
||||
}
|
||||
|
||||
// Display existing attachments
|
||||
if (data.attachment_names) {
|
||||
const names = data.attachment_names.split('|||');
|
||||
let html = '<div class="mt-2"><p class="mb-1 fw-bold small">المرفقات الحالية:</p><ul class="list-unstyled small">';
|
||||
names.forEach(name => {
|
||||
html += `<li><i class="fas fa-file-alt me-1 text-muted"></i> ${name}</li>`;
|
||||
});
|
||||
html += '</ul></div>';
|
||||
existingAttachmentsDiv.innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
mailModal.show();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initEditors().finally(() => {
|
||||
<?php if ($deepLinkData): ?>
|
||||
openMailModal('edit', <?= json_encode($deepLinkData) ?>);
|
||||
<?php elseif ($error && isset($_POST['action'])): ?>
|
||||
const errorData = <?= json_encode($_POST) ?>;
|
||||
openMailModal(errorData.action, errorData);
|
||||
<?php endif; ?>
|
||||
});
|
||||
|
||||
// Handle file selection display
|
||||
const attachmentsInput = document.getElementById('modalAttachmentsInput');
|
||||
if (attachmentsInput) {
|
||||
attachmentsInput.addEventListener('change', function() {
|
||||
const fileList = this.files;
|
||||
const selectedAttachmentsDiv = document.getElementById('modalSelectedAttachments');
|
||||
selectedAttachmentsDiv.innerHTML = '';
|
||||
|
||||
if (fileList.length > 0) {
|
||||
let html = '<div class="mt-2"><p class="mb-1 fw-bold small text-primary">المرفقات المختارة للرفع:</p><ul class="list-unstyled small">';
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const fileSize = (fileList[i].size / 1024).toFixed(1);
|
||||
html += `<li><i class="fas fa-file-upload me-1 text-primary"></i> ${fileList[i].name} <span class="text-muted">(${fileSize} KB)</span></li>`;
|
||||
}
|
||||
html += '</ul></div>';
|
||||
selectedAttachmentsDiv.innerHTML = html;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('mailForm').addEventListener('submit', function() {
|
||||
if (typeof tinymce !== 'undefined') {
|
||||
tinymce.triggerSave();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
160
overdue_report.php
Normal file
160
overdue_report.php
Normal file
@ -0,0 +1,160 @@
|
||||
<?php
|
||||
require_once 'includes/header.php';
|
||||
|
||||
if (!canView('reports')) {
|
||||
redirect('index.php');
|
||||
}
|
||||
|
||||
$type_filter = $_GET['type'] ?? '';
|
||||
$user_filter = $_GET['user_id'] ?? '';
|
||||
|
||||
$overdue_items = [];
|
||||
$queries = [];
|
||||
|
||||
if (!$type_filter || $type_filter === 'inbound') {
|
||||
$where = ["m.due_date < CURDATE()", "s.name != 'closed'"];
|
||||
$params = [];
|
||||
if ($user_filter) {
|
||||
$where[] = "m.assigned_to = ?";
|
||||
$params[] = $user_filter;
|
||||
}
|
||||
$where_clause = implode(" AND ", $where);
|
||||
$sql = "SELECT m.*, 'inbound' as type, u.full_name as assigned_name, s.name as status_name, s.color as status_color
|
||||
FROM inbound_mail m
|
||||
LEFT JOIN users u ON m.assigned_to = u.id
|
||||
LEFT JOIN mailbox_statuses s ON m.status_id = s.id
|
||||
WHERE $where_clause";
|
||||
$stmt = db()->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$overdue_items = array_merge($overdue_items, $stmt->fetchAll());
|
||||
}
|
||||
|
||||
if (!$type_filter || $type_filter === 'outbound') {
|
||||
$where = ["m.due_date < CURDATE()", "s.name != 'closed'"];
|
||||
$params = [];
|
||||
if ($user_filter) {
|
||||
$where[] = "m.assigned_to = ?";
|
||||
$params[] = $user_filter;
|
||||
}
|
||||
$where_clause = implode(" AND ", $where);
|
||||
$sql = "SELECT m.*, 'outbound' as type, u.full_name as assigned_name, s.name as status_name, s.color as status_color
|
||||
FROM outbound_mail m
|
||||
LEFT JOIN users u ON m.assigned_to = u.id
|
||||
LEFT JOIN mailbox_statuses s ON m.status_id = s.id
|
||||
WHERE $where_clause";
|
||||
$stmt = db()->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$overdue_items = array_merge($overdue_items, $stmt->fetchAll());
|
||||
}
|
||||
|
||||
// Sort by due date
|
||||
usort($overdue_items, function($a, $b) {
|
||||
return strtotime($a['due_date']) - strtotime($b['due_date']);
|
||||
});
|
||||
|
||||
// Fetch all users for filter
|
||||
$users = db()->query("SELECT id, full_name FROM users ORDER BY full_name")->fetchAll();
|
||||
|
||||
function getStatusBadgeForReport($item) {
|
||||
$status_name = $item['status_name'] ?? 'غير معروف';
|
||||
$status_color = $item['status_color'] ?? '#6c757d';
|
||||
|
||||
$display_name = $status_name;
|
||||
if ($status_name == 'received') $display_name = 'تم الاستلام';
|
||||
if ($status_name == 'in_progress') $display_name = 'قيد المعالجة';
|
||||
if ($status_name == 'closed') $display_name = 'مكتمل';
|
||||
|
||||
return '<span class="badge" style="background-color: ' . $status_color . ';">' . htmlspecialchars($display_name) . '</span>';
|
||||
}
|
||||
?>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<form method="GET" class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">نوع البريد</label>
|
||||
<select name="type" class="form-select">
|
||||
<option value="">الكل</option>
|
||||
<option value="inbound" <?= $type_filter == 'inbound' ? 'selected' : '' ?>>وارد</option>
|
||||
<option value="outbound" <?= $type_filter == 'outbound' ? 'selected' : '' ?>>صادر</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">الموظف المسؤول</label>
|
||||
<select name="user_id" class="form-select">
|
||||
<option value="">الكل</option>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<option value="<?= $user['id'] ?>" <?= $user_filter == $user['id'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($user['full_name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100">تصفية</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h5 class="card-title mb-0"><i class="fas fa-exclamation-triangle me-2"></i> جميع المهام المتأخرة</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>رقم المرجع</th>
|
||||
<th>النوع</th>
|
||||
<th>الموضوع</th>
|
||||
<th>الموظف المسؤول</th>
|
||||
<th>الحالة</th>
|
||||
<th>تاريخ الاستحقاق</th>
|
||||
<th>الأيام المتأخرة</th>
|
||||
<th>الإجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($overdue_items)): ?>
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-4 text-muted">لا توجد مهام متأخرة حالياً.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($overdue_items as $item):
|
||||
$due_date = new DateTime($item['due_date']);
|
||||
$today = new DateTime();
|
||||
$diff = $today->diff($due_date)->format("%a");
|
||||
?>
|
||||
<tr>
|
||||
<td class="fw-bold"><?= htmlspecialchars($item['ref_no']) ?></td>
|
||||
<td>
|
||||
<span class="badge bg-<?= $item['type'] == 'inbound' ? 'info' : 'warning' ?>">
|
||||
<?= $item['type'] == 'inbound' ? 'وارد' : 'صادر' ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($item['subject']) ?></td>
|
||||
<td><?= htmlspecialchars($item['assigned_name'] ?? 'غير معين') ?></td>
|
||||
<td><?= getStatusBadgeForReport($item) ?></td>
|
||||
<td class="text-danger fw-bold"><?= $item['due_date'] ?></td>
|
||||
<td class="text-danger fw-bold"><?= $diff ?> يوم</td>
|
||||
<td>
|
||||
<a href="view_mail.php?id=<?= $item['id'] ?>&type=<?= $item['type'] ?>" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-eye"></i> عرض
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
319
print_inbound.php
Normal file
319
print_inbound.php
Normal file
@ -0,0 +1,319 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/settings.php';
|
||||
|
||||
// Check if user is logged in
|
||||
session_start();
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
die('Access Denied');
|
||||
}
|
||||
|
||||
// Basic permission check
|
||||
function isAdmin() {
|
||||
if (isset($_SESSION['is_super_admin']) && $_SESSION['is_super_admin'] == 1) return true;
|
||||
if (isset($_SESSION['user_role']) && strtolower($_SESSION['user_role']) === 'admin') return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function canView($page) {
|
||||
if (isAdmin()) return true;
|
||||
return $_SESSION['permissions'][$page]['view'] ?? false;
|
||||
}
|
||||
|
||||
if (!canView('inbound')) {
|
||||
die('Unauthorized access');
|
||||
}
|
||||
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
if (!$id) {
|
||||
die('Invalid ID');
|
||||
}
|
||||
|
||||
// Fetch inbound mail details
|
||||
$stmt = db()->prepare("SELECT * FROM inbound_mail WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$mail = $stmt->fetch();
|
||||
|
||||
if (!$mail) {
|
||||
die('Mail not found');
|
||||
}
|
||||
|
||||
$settings = get_settings();
|
||||
$logo = !empty($settings['site_logo']) ? $settings['site_logo'] : '';
|
||||
$site_name = $settings['site_name'];
|
||||
$site_address = $settings['site_address'];
|
||||
$site_slogan = $settings['site_slogan'];
|
||||
|
||||
/**
|
||||
* Convert Gregorian date to Hijri
|
||||
*/
|
||||
function gregorianToHijri($date) {
|
||||
if (!$date) return '';
|
||||
$time = strtotime($date);
|
||||
$m = date('m', $time);
|
||||
$d = date('d', $time);
|
||||
$y = date('Y', $time);
|
||||
|
||||
if (($y > 1582) || (($y == 1582) && ($m > 10)) || (($y == 1582) && ($m == 10) && ($d > 14))) {
|
||||
$jd = (int)((1461 * ($y + 4800 + (int)(($m - 14) / 12))) / 4) +
|
||||
(int)((367 * ($m - 2 - 12 * ((int)(($m - 14) / 12)))) / 12) -
|
||||
(int)((3 * ((int)(($y + 4900 + (int)(($m - 14) / 12)) / 100))) / 4) +
|
||||
$d - 32075;
|
||||
} else {
|
||||
$jd = 367 * $y - (int)((7 * ($y + 5001 + (int)(($m - 9) / 7))) / 4) + (int)((275 * $m) / 9) + $d + 1729777;
|
||||
}
|
||||
|
||||
$l = $jd - 1948440 + 10632;
|
||||
$n = (int)(($l - 1) / 10631);
|
||||
$l = $l - 10631 * $n + 354;
|
||||
$j = ((int)((10985 - $l) / 5316)) * ((int)((50 * $l) / 17719)) + ((int)($l / 5670)) * ((int)((43 * $l) / 15238));
|
||||
$l = $l - ((int)((30 - $j) / 15)) * ((int)((17719 * $j) / 50)) - ((int)($j / 16)) * ((int)((15238 * $j) / 43)) + 29;
|
||||
|
||||
$month = (int)((24 * $l) / 709);
|
||||
$day = $l - (int)((709 * $month) / 24);
|
||||
$year = 30 * $n + $j - 30;
|
||||
|
||||
$hijriMonths = [
|
||||
1 => "محرم", 2 => "صفر", 3 => "ربيع الأول", 4 => "ربيع الآخر",
|
||||
5 => "جمادى الأولى", 6 => "جمادى الآخرة", 7 => "رجب", 8 => "شعبان",
|
||||
9 => "رمضان", 10 => "شوال", 11 => "ذو القعدة", 12 => "ذو الحجة"
|
||||
];
|
||||
|
||||
return $day . ' ' . $hijriMonths[$month] . ' ' . $year . ' هـ';
|
||||
}
|
||||
|
||||
$hijriDate = gregorianToHijri($mail['date_registered']);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>طباعة بريد وارد - <?= htmlspecialchars($mail['ref_no']) ?></title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 0;
|
||||
}
|
||||
body {
|
||||
font-family: 'Cairo', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f4f7f6;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.print-wrapper {
|
||||
width: 21cm;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
padding: 1.5cm;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
min-height: 29.7cm;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Repeating Header */
|
||||
.report-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.report-header {
|
||||
display: table-header-group;
|
||||
}
|
||||
.report-footer {
|
||||
display: table-footer-group;
|
||||
}
|
||||
|
||||
/* Header Style */
|
||||
.header-container {
|
||||
border-bottom: 3px double #00827F;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 2px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.header-logo img {
|
||||
max-height: 80px;
|
||||
}
|
||||
.header-info { text-align: left; }
|
||||
.site-name { font-size: 24px; font-weight: bold; color: #00827F; margin-bottom: 2px; }
|
||||
.site-slogan { font-size: 14px; font-weight: bold; color: #00827F; margin-bottom: 2px; }
|
||||
|
||||
/* Meta & Content */
|
||||
.mail-meta {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background: transparent;
|
||||
padding: 2px 0;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
.mail-content {
|
||||
font-size: 18px;
|
||||
text-align: justify;
|
||||
color: #000;
|
||||
min-height: 12cm;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
/* Footer Style */
|
||||
.footer-container {
|
||||
border-top: 3px double #00827F;
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
margin-top: auto;
|
||||
}
|
||||
.contact-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 5px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.page-num-display {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* Footer Spacer for Table */
|
||||
.footer-spacer {
|
||||
height: 80px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* UI Controls */
|
||||
.no-print {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background: #00827F;
|
||||
color: #fff;
|
||||
padding: 10px 25px;
|
||||
border: none;
|
||||
border-radius: 30px;
|
||||
cursor: pointer;
|
||||
z-index: 1000;
|
||||
font-family: 'Cairo', sans-serif;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
@media print {
|
||||
@page {
|
||||
margin: 1cm 0;
|
||||
}
|
||||
body { background: #fff; }
|
||||
.no-print { display: none; }
|
||||
.print-wrapper {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0 1.5cm;
|
||||
box-shadow: none;
|
||||
min-height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.footer-container {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 10px 1.5cm;
|
||||
box-sizing: border-box;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.footer-spacer {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<button class="no-print" onclick="window.print()">
|
||||
<i class="fas fa-print"></i> طباعة الوثيقة
|
||||
</button>
|
||||
|
||||
<div class="print-wrapper">
|
||||
<table class="report-table">
|
||||
<thead class="report-header">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="header-container">
|
||||
<div class="header-logo">
|
||||
<?php if ($logo): ?>
|
||||
<img src="<?= htmlspecialchars($logo) ?>" alt="Logo">
|
||||
<?php else: ?>
|
||||
<div style="font-weight: bold; font-size: 26px; color: #00827F;"><?= htmlspecialchars($site_name) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="header-info">
|
||||
<div class="site-name"><?= htmlspecialchars($site_name) ?></div>
|
||||
<?php if (!empty($site_slogan)): ?>
|
||||
<div class="site-slogan"><?= htmlspecialchars($site_slogan) ?></div>
|
||||
<?php endif; ?>
|
||||
<div style="font-size: 12px; color: #666;"><?= htmlspecialchars($site_address) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="mail-meta">
|
||||
<div><strong>رقم القيد:</strong> <?= htmlspecialchars($mail['ref_no']) ?></div>
|
||||
<div><strong>التاريخ:</strong> <?= htmlspecialchars($hijriDate) ?> | <?= htmlspecialchars($mail['date_registered']) ?>م</div>
|
||||
</div>
|
||||
|
||||
<div class="mail-content">
|
||||
<?= $mail['description'] ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tfoot class="report-footer">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="footer-spacer"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<div class="footer-container">
|
||||
<div class="contact-row">
|
||||
<span><i class="fas fa-phone"></i> 99621515</span>
|
||||
<span><i class="fas fa-envelope"></i> ahlalhkair@gmail.com</span>
|
||||
<span><i class="fas fa-globe"></i> https://alkhairteam.net/</span>
|
||||
<span class="ms-3">
|
||||
<i class="fab fa-instagram"></i>
|
||||
<i class="fab fa-twitter"></i>
|
||||
<i class="fab fa-facebook"></i>
|
||||
alkhair_team
|
||||
</span>
|
||||
</div>
|
||||
<div class="page-num-display">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
272
print_meeting.php
Normal file
272
print_meeting.php
Normal file
@ -0,0 +1,272 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/settings.php';
|
||||
require_once __DIR__ . '/includes/permissions.php';
|
||||
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header("Location: login.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!canView('meetings')) {
|
||||
die("ليس لديك صلاحية لعرض هذه الصفحة");
|
||||
}
|
||||
|
||||
$id = $_GET['id'] ?? 0;
|
||||
|
||||
if (!$id) {
|
||||
die("رقم الاجتماع غير صحيح");
|
||||
}
|
||||
|
||||
try {
|
||||
$db = db();
|
||||
$stmt = $db->prepare("SELECT m.*, u.username as created_by_name FROM meetings m LEFT JOIN users u ON m.created_by = u.id WHERE m.id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$meeting = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$meeting) {
|
||||
die("الاجتماع غير موجود");
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
die("خطأ في قاعدة البيانات");
|
||||
}
|
||||
|
||||
$settings = get_settings();
|
||||
// Fix: Use site_logo directly as it contains the full path, or fall back correctly
|
||||
$logo_path = $settings['site_logo'] ?? '';
|
||||
|
||||
// Validation and fallback logic
|
||||
if (empty($logo_path) || !file_exists($logo_path)) {
|
||||
// If it's just a filename without path, try adding the path
|
||||
if (!empty($logo_path) && file_exists('uploads/charity/' . $logo_path)) {
|
||||
$logo_path = 'uploads/charity/' . $logo_path;
|
||||
} else {
|
||||
// Try to find any logo in the directory if the specific one is missing
|
||||
$possible_logos = glob('uploads/charity/*logo*.*');
|
||||
if (!empty($possible_logos)) {
|
||||
$logo_path = $possible_logos[0];
|
||||
} else {
|
||||
$logo_path = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($logo_path) {
|
||||
$logo_html = '<img src="' . $logo_path . '" alt="Logo" style="max-height: 100px;">';
|
||||
} else {
|
||||
$logo_html = '';
|
||||
}
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>محضر اجتماع - <?= htmlspecialchars($meeting['title']) ?></title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
direction: rtl;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
border: 1px solid #ddd;
|
||||
padding: 40px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #333;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 10px 0 5px;
|
||||
font-size: 24px;
|
||||
}
|
||||
.header h2 {
|
||||
margin: 5px 0;
|
||||
font-size: 18px;
|
||||
color: #555;
|
||||
}
|
||||
.header .meta {
|
||||
font-size: 14px;
|
||||
color: #777;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
background: #f4f4f4;
|
||||
padding: 8px 15px;
|
||||
border-right: 4px solid #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.info-item {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.info-label {
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
}
|
||||
.content-box {
|
||||
padding: 10px;
|
||||
border: 1px solid #eee;
|
||||
background: #fafafa;
|
||||
min-height: 50px;
|
||||
}
|
||||
/* Style for rich text content to ensure it looks good */
|
||||
.rich-text {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.rich-text p { margin-top: 0; }
|
||||
.rich-text ul, .rich-text ol { margin-right: 20px; padding-right: 0; }
|
||||
|
||||
/* Quill Alignment Classes */
|
||||
.ql-align-center { text-align: center; }
|
||||
.ql-align-right { text-align: right; }
|
||||
.ql-align-justify { text-align: justify; }
|
||||
.ql-direction-rtl { direction: rtl; text-align: right; }
|
||||
.ql-direction-ltr { direction: ltr; text-align: left; }
|
||||
.ql-indent-1 { margin-right: 3em; } /* RTL indent */
|
||||
.ql-indent-2 { margin-right: 6em; }
|
||||
|
||||
.signatures {
|
||||
margin-top: 50px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.signature-box {
|
||||
width: 40%;
|
||||
text-align: center;
|
||||
}
|
||||
.signature-line {
|
||||
margin-top: 50px;
|
||||
border-top: 1px solid #000;
|
||||
}
|
||||
@media print {
|
||||
body {
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
}
|
||||
.container {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
.no-print {
|
||||
display: none;
|
||||
}
|
||||
button { display: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="no-print" style="text-align: center; margin-bottom: 20px;">
|
||||
<button onclick="window.print()" style="padding: 10px 20px; font-size: 16px; cursor: pointer; background: #333; color: #fff; border: none; border-radius: 5px;">طباعة المحضر</button>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<?php if ($logo_html): ?>
|
||||
<div style="margin-bottom: 10px;"><?= $logo_html ?></div>
|
||||
<?php endif; ?>
|
||||
<h1><?= htmlspecialchars($settings['site_name']) ?></h1>
|
||||
<?php if ($settings['site_slogan']): ?>
|
||||
<h2><?= htmlspecialchars($settings['site_slogan']) ?></h2>
|
||||
<?php endif; ?>
|
||||
<div style="margin-top: 20px; font-size: 20px; font-weight: bold; text-decoration: underline;">محضر اجتماع رسمي</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="info-label">عنوان الاجتماع:</span>
|
||||
<?= htmlspecialchars($meeting['title']) ?>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">المنظم:</span>
|
||||
<?= htmlspecialchars($meeting['created_by_name']) ?>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">التاريخ:</span>
|
||||
<?= date('Y-m-d', strtotime($meeting['start_time'])) ?>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">الوقت:</span>
|
||||
<?= date('H:i', strtotime($meeting['start_time'])) ?> - <?= date('H:i', strtotime($meeting['end_time'])) ?>
|
||||
</div>
|
||||
<div class="info-item" style="grid-column: span 2;">
|
||||
<span class="info-label">المكان:</span>
|
||||
<?= htmlspecialchars($meeting['location'] ?: 'غير محدد') ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($meeting['agenda']): ?>
|
||||
<div class="section">
|
||||
<div class="section-title">جدول الأعمال</div>
|
||||
<div class="content-box rich-text"><?= $meeting['agenda'] ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="section">
|
||||
<div class="info-grid">
|
||||
<div>
|
||||
<div class="section-title">الحضور</div>
|
||||
<div class="content-box"><?= nl2br(htmlspecialchars($meeting['attendees'] ?: 'لا يوجد')) ?></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="section-title">الغياب / الاعتذار</div>
|
||||
<div class="content-box"><?= nl2br(htmlspecialchars($meeting['absentees'] ?: 'لا يوجد')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($meeting['meeting_details']): ?>
|
||||
<div class="section">
|
||||
<div class="section-title">تفاصيل الاجتماع / المحضر</div>
|
||||
<div class="content-box rich-text" style="min-height: 150px;"><?= $meeting['meeting_details'] ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($meeting['description']): ?>
|
||||
<div class="section">
|
||||
<div class="section-title"> القرارات والإلتزامات</div>
|
||||
<div class="content-box rich-text"><?= $meeting['description'] ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="signatures">
|
||||
<div class="signature-box">
|
||||
<div>توقيع مقرر الاجتماع</div>
|
||||
<div class="signature-line"></div>
|
||||
</div>
|
||||
<div class="signature-box">
|
||||
<div>اعتماد المدير / الرئيس</div>
|
||||
<div class="signature-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px; text-align: center; font-size: 12px; color: #999;">
|
||||
تم استخراج هذا المستند إلكترونياً من النظام بتاريخ <?= date('Y-m-d H:i') ?>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
319
print_outbound.php
Normal file
319
print_outbound.php
Normal file
@ -0,0 +1,319 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/settings.php';
|
||||
|
||||
// Check if user is logged in
|
||||
session_start();
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
die('Access Denied');
|
||||
}
|
||||
|
||||
// Basic permission check
|
||||
function isAdmin() {
|
||||
if (isset($_SESSION['is_super_admin']) && $_SESSION['is_super_admin'] == 1) return true;
|
||||
if (isset($_SESSION['user_role']) && strtolower($_SESSION['user_role']) === 'admin') return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function canView($page) {
|
||||
if (isAdmin()) return true;
|
||||
return $_SESSION['permissions'][$page]['view'] ?? false;
|
||||
}
|
||||
|
||||
if (!canView('outbound')) {
|
||||
die('Unauthorized access');
|
||||
}
|
||||
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
if (!$id) {
|
||||
die('Invalid ID');
|
||||
}
|
||||
|
||||
// Fetch outbound mail details
|
||||
$stmt = db()->prepare("SELECT * FROM outbound_mail WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$mail = $stmt->fetch();
|
||||
|
||||
if (!$mail) {
|
||||
die('Mail not found');
|
||||
}
|
||||
|
||||
$settings = get_settings();
|
||||
$logo = !empty($settings['site_logo']) ? $settings['site_logo'] : '';
|
||||
$site_name = $settings['site_name'];
|
||||
$site_address = $settings['site_address'];
|
||||
$site_slogan = $settings['site_slogan'];
|
||||
|
||||
/**
|
||||
* Convert Gregorian date to Hijri
|
||||
*/
|
||||
function gregorianToHijri($date) {
|
||||
if (!$date) return '';
|
||||
$time = strtotime($date);
|
||||
$m = date('m', $time);
|
||||
$d = date('d', $time);
|
||||
$y = date('Y', $time);
|
||||
|
||||
if (($y > 1582) || (($y == 1582) && ($m > 10)) || (($y == 1582) && ($m == 10) && ($d > 14))) {
|
||||
$jd = (int)((1461 * ($y + 4800 + (int)(($m - 14) / 12))) / 4) +
|
||||
(int)((367 * ($m - 2 - 12 * ((int)(($m - 14) / 12)))) / 12) -
|
||||
(int)((3 * ((int)(($y + 4900 + (int)(($m - 14) / 12)) / 100))) / 4) +
|
||||
$d - 32075;
|
||||
} else {
|
||||
$jd = 367 * $y - (int)((7 * ($y + 5001 + (int)(($m - 9) / 7))) / 4) + (int)((275 * $m) / 9) + $d + 1729777;
|
||||
}
|
||||
|
||||
$l = $jd - 1948440 + 10632;
|
||||
$n = (int)(($l - 1) / 10631);
|
||||
$l = $l - 10631 * $n + 354;
|
||||
$j = ((int)((10985 - $l) / 5316)) * ((int)((50 * $l) / 17719)) + ((int)($l / 5670)) * ((int)((43 * $l) / 15238));
|
||||
$l = $l - ((int)((30 - $j) / 15)) * ((int)((17719 * $j) / 50)) - ((int)($j / 16)) * ((int)((15238 * $j) / 43)) + 29;
|
||||
|
||||
$month = (int)((24 * $l) / 709);
|
||||
$day = $l - (int)((709 * $month) / 24);
|
||||
$year = 30 * $n + $j - 30;
|
||||
|
||||
$hijriMonths = [
|
||||
1 => "محرم", 2 => "صفر", 3 => "ربيع الأول", 4 => "ربيع الآخر",
|
||||
5 => "جمادى الأولى", 6 => "جمادى الآخرة", 7 => "رجب", 8 => "شعبان",
|
||||
9 => "رمضان", 10 => "شوال", 11 => "ذو القعدة", 12 => "ذو الحجة"
|
||||
];
|
||||
|
||||
return $day . ' ' . $hijriMonths[$month] . ' ' . $year . ' هـ';
|
||||
}
|
||||
|
||||
$hijriDate = gregorianToHijri($mail['date_registered']);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>طباعة بريد صادر - <?= htmlspecialchars($mail['ref_no']) ?></title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 0;
|
||||
}
|
||||
body {
|
||||
font-family: 'Cairo', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f4f7f6;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.print-wrapper {
|
||||
width: 21cm;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
padding: 1.5cm;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
min-height: 29.7cm;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Repeating Header */
|
||||
.report-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.report-header {
|
||||
display: table-header-group;
|
||||
}
|
||||
.report-footer {
|
||||
display: table-footer-group;
|
||||
}
|
||||
|
||||
/* Header Style */
|
||||
.header-container {
|
||||
border-bottom: 3px double #00827F;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 2px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.header-logo img {
|
||||
max-height: 80px;
|
||||
}
|
||||
.header-info { text-align: left; }
|
||||
.site-name { font-size: 24px; font-weight: bold; color: #00827F; margin-bottom: 2px; }
|
||||
.site-slogan { font-size: 14px; font-weight: bold; color: #00827F; margin-bottom: 2px; }
|
||||
|
||||
/* Meta & Content */
|
||||
.mail-meta {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background: transparent;
|
||||
padding: 2px 0;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
.mail-content {
|
||||
font-size: 18px;
|
||||
text-align: justify;
|
||||
color: #000;
|
||||
min-height: 12cm;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
/* Footer Style */
|
||||
.footer-container {
|
||||
border-top: 3px double #00827F;
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
margin-top: auto;
|
||||
}
|
||||
.contact-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 5px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.page-num-display {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* Footer Spacer for Table */
|
||||
.footer-spacer {
|
||||
height: 80px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* UI Controls */
|
||||
.no-print {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background: #00827F;
|
||||
color: #fff;
|
||||
padding: 10px 25px;
|
||||
border: none;
|
||||
border-radius: 30px;
|
||||
cursor: pointer;
|
||||
z-index: 1000;
|
||||
font-family: 'Cairo', sans-serif;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
@media print {
|
||||
@page {
|
||||
margin: 1cm 0;
|
||||
}
|
||||
body { background: #fff; }
|
||||
.no-print { display: none; }
|
||||
.print-wrapper {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0 1.5cm;
|
||||
box-shadow: none;
|
||||
min-height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.footer-container {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 10px 1.5cm;
|
||||
box-sizing: border-box;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.footer-spacer {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<button class="no-print" onclick="window.print()">
|
||||
<i class="fas fa-print"></i> طباعة الوثيقة
|
||||
</button>
|
||||
|
||||
<div class="print-wrapper">
|
||||
<table class="report-table">
|
||||
<thead class="report-header">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="header-container">
|
||||
<div class="header-logo">
|
||||
<?php if ($logo): ?>
|
||||
<img src="<?= htmlspecialchars($logo) ?>" alt="Logo">
|
||||
<?php else: ?>
|
||||
<div style="font-weight: bold; font-size: 26px; color: #00827F;"><?= htmlspecialchars($site_name) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="header-info">
|
||||
<div class="site-name"><?= htmlspecialchars($site_name) ?></div>
|
||||
<?php if (!empty($site_slogan)): ?>
|
||||
<div class="site-slogan"><?= htmlspecialchars($site_slogan) ?></div>
|
||||
<?php endif; ?>
|
||||
<div style="font-size: 12px; color: #666;"><?= htmlspecialchars($site_address) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="mail-meta">
|
||||
<div><strong>رقم القيد:</strong> <?= htmlspecialchars($mail['ref_no']) ?></div>
|
||||
<div><strong>التاريخ:</strong> <?= htmlspecialchars($hijriDate) ?> | <?= htmlspecialchars($mail['date_registered']) ?>م</div>
|
||||
</div>
|
||||
|
||||
<div class="mail-content">
|
||||
<?= $mail['description'] ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tfoot class="report-footer">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="footer-spacer"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<div class="footer-container">
|
||||
<div class="contact-row">
|
||||
<span><i class="fas fa-phone"></i> 99621515</span>
|
||||
<span><i class="fas fa-envelope"></i> ahlalhkair@gmail.com</span>
|
||||
<span><i class="fas fa-globe"></i> https://alkhairteam.net/</span>
|
||||
<span class="ms-3">
|
||||
<i class="fab fa-instagram"></i>
|
||||
<i class="fab fa-twitter"></i>
|
||||
<i class="fab fa-facebook"></i>
|
||||
alkhair_team
|
||||
</span>
|
||||
</div>
|
||||
<div class="page-num-display">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
117
profile.php
Normal file
117
profile.php
Normal file
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$success_msg = '';
|
||||
$error_msg = '';
|
||||
|
||||
// Fetch current user data
|
||||
$stmt = db()->prepare("SELECT * FROM users WHERE id = ?");
|
||||
$stmt->execute([$user_id]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_POST['update_profile'])) {
|
||||
$full_name = $_POST['full_name'];
|
||||
$email = $_POST['email'];
|
||||
$phone = $_POST['phone'];
|
||||
$address = $_POST['address'];
|
||||
$password = $_POST['password'];
|
||||
$profile_image = $user['profile_image'];
|
||||
|
||||
// Handle Profile Image Upload
|
||||
if (isset($_FILES['profile_image']) && $_FILES['profile_image']['error'] === UPLOAD_ERR_OK) {
|
||||
$upload_dir = 'uploads/profiles/';
|
||||
if (!is_dir($upload_dir)) mkdir($upload_dir, 0775, true);
|
||||
|
||||
$file_ext = pathinfo($_FILES['profile_image']['name'], PATHINFO_EXTENSION);
|
||||
$new_file_name = time() . '_u' . $user_id . '.' . $file_ext;
|
||||
$target_file = $upload_dir . $new_file_name;
|
||||
|
||||
if (move_uploaded_file($_FILES['profile_image']['tmp_name'], $target_file)) {
|
||||
$profile_image = $target_file;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($password)) {
|
||||
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
|
||||
$stmt = db()->prepare("UPDATE users SET full_name = ?, email = ?, phone = ?, address = ?, password = ?, profile_image = ? WHERE id = ?");
|
||||
$stmt->execute([$full_name, $email, $phone, $address, $hashed_password, $profile_image, $user_id]);
|
||||
} else {
|
||||
$stmt = db()->prepare("UPDATE users SET full_name = ?, email = ?, phone = ?, address = ?, profile_image = ? WHERE id = ?");
|
||||
$stmt->execute([$full_name, $email, $phone, $address, $profile_image, $user_id]);
|
||||
}
|
||||
$_SESSION['success'] = 'تم تحديث الملف الشخصي بنجاح';
|
||||
redirect('profile.php');
|
||||
}
|
||||
}
|
||||
|
||||
// Get session messages
|
||||
if (isset($_SESSION['success'])) {
|
||||
$success_msg = $_SESSION['success'];
|
||||
unset($_SESSION['success']);
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-4">
|
||||
<h2 class="fw-bold"><i class="fas fa-user-circle me-2"></i> الملف الشخصي</h2>
|
||||
</div>
|
||||
|
||||
<?php if ($success_msg): ?>
|
||||
<div class="alert alert-success"><?= $success_msg ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card p-4">
|
||||
<h4 class="mb-4">تعديل الملف الشخصي</h4>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<div class="text-center mb-4">
|
||||
<?php if ($user['profile_image']): ?>
|
||||
<img src="<?= $user['profile_image'] ?>" alt="Profile" class="rounded-circle shadow" style="width: 150px; height: 150px; object-fit: cover; border: 3px solid #0d6efd;">
|
||||
<?php else: ?>
|
||||
<div class="rounded-circle bg-light d-inline-flex align-items-center justify-content-center shadow" style="width: 150px; height: 150px; border: 3px solid #ddd;">
|
||||
<i class="fas fa-user fa-5x text-secondary"></i>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">الاسم الكامل</label>
|
||||
<input type="text" name="full_name" class="form-control" value="<?= htmlspecialchars($user['full_name'] ?? '') ?>" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">البريد الإلكتروني</label>
|
||||
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars($user['email'] ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">رقم الهاتف</label>
|
||||
<input type="text" name="phone" class="form-control" value="<?= htmlspecialchars($user['phone'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">الصورة الشخصية</label>
|
||||
<input type="file" name="profile_image" class="form-control" accept="image/*">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">العنوان</label>
|
||||
<textarea name="address" class="form-control" rows="3"><?= htmlspecialchars($user['address'] ?? '') ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">كلمة المرور الجديدة (اتركها فارغة إذا لم ترغب في التغيير)</label>
|
||||
<input type="password" name="password" class="form-control">
|
||||
</div>
|
||||
|
||||
<button type="submit" name="update_profile" class="btn btn-primary w-100 mt-3">حفظ التغييرات</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
31
scripts/seed_accounts.php
Normal file
31
scripts/seed_accounts.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
$pdo = db();
|
||||
|
||||
$common_accounts = [
|
||||
['النقدية', 'أصول'],
|
||||
['حسابات القبض', 'أصول'],
|
||||
['المخزون', 'أصول'],
|
||||
['الأصول الثابتة', 'أصول'],
|
||||
['حسابات الدفع', 'خصوم'],
|
||||
['قروض قصيرة الأجل', 'خصوم'],
|
||||
['قروض طويلة الأجل', 'خصوم'],
|
||||
['رأس المال', 'حقوق ملكية'],
|
||||
['الأرباح المحتجزة', 'حقوق ملكية'],
|
||||
['إيرادات المبيعات', 'إيرادات'],
|
||||
['إيرادات أخرى', 'إيرادات'],
|
||||
['تكلفة البضاعة المباعة', 'مصروفات'],
|
||||
['مصروفات الرواتب', 'مصروفات'],
|
||||
['مصروفات الإيجار', 'مصروفات'],
|
||||
['مصروفات المرافق', 'مصروفات'],
|
||||
['مصروفات التسويق', 'مصروفات']
|
||||
];
|
||||
|
||||
$stmt = $pdo->prepare("INSERT IGNORE INTO accounting_accounts (name, type) VALUES (?, ?)");
|
||||
|
||||
foreach ($common_accounts as $account) {
|
||||
$stmt->execute($account);
|
||||
}
|
||||
|
||||
echo "تمت إضافة الحسابات الشائعة بنجاح.";
|
||||
86
scripts/send_reminders.php
Normal file
86
scripts/send_reminders.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
// scripts/send_reminders.php
|
||||
// Should be run as a cron job daily: php scripts/send_reminders.php
|
||||
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
require_once __DIR__ . '/../m_services/MailService.php';
|
||||
|
||||
echo "[" . date('Y-m-d H:i:s') . "] Starting reminder process..." . PHP_EOL;
|
||||
|
||||
$tables = ['inbound_mail', 'outbound_mail']; // internal mail usually doesn't have due dates or reminders the same way
|
||||
|
||||
foreach ($tables as $table) {
|
||||
echo "Processing $table..." . PHP_EOL;
|
||||
|
||||
// 1. Tasks due in 24 hours
|
||||
$stmt = db()->prepare("
|
||||
SELECT m.*, u.email, u.full_name
|
||||
FROM $table m
|
||||
JOIN users u ON m.assigned_to = u.id
|
||||
JOIN mailbox_statuses s ON m.status_id = s.id
|
||||
WHERE m.due_date = DATE_ADD(CURDATE(), INTERVAL 1 DAY)
|
||||
AND s.name != 'closed'
|
||||
");
|
||||
$stmt->execute();
|
||||
$due_tomorrow = $stmt->fetchAll();
|
||||
|
||||
foreach ($due_tomorrow as $task) {
|
||||
if (!empty($task['email'])) {
|
||||
$subject = "تذكير: موعد نهائي لمهمة غداً - " . $task['ref_no'];
|
||||
$html = "
|
||||
<div dir='rtl' style='font-family: Arial, sans-serif;'>
|
||||
<h3>تذكير بموعد نهائي</h3>
|
||||
<p>عزيزي <b>" . htmlspecialchars($task['full_name']) . "</b>،</p>
|
||||
<p>هذا تذكير بأن المهمة التالية مستحقة غداً:</p>
|
||||
<ul>
|
||||
<li><b>رقم المرجع:</b> " . htmlspecialchars($task['ref_no']) . "</li>
|
||||
<li><b>الموضوع:</b> " . htmlspecialchars($task['subject']) . "</li>
|
||||
<li><b>تاريخ الاستحقاق:</b> " . $task['due_date'] . "</li>
|
||||
</ul>
|
||||
<p>يرجى متابعة المهمة وإغلاقها في الوقت المحدد.</p>
|
||||
</div>
|
||||
";
|
||||
$res = MailService::sendMail($task['email'], $subject, $html);
|
||||
if ($res['success']) {
|
||||
echo "Sent 24h reminder to " . $task['email'] . " for task " . $task['ref_no'] . PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Overdue tasks
|
||||
$stmt = db()->prepare("
|
||||
SELECT m.*, u.email, u.full_name
|
||||
FROM $table m
|
||||
JOIN users u ON m.assigned_to = u.id
|
||||
JOIN mailbox_statuses s ON m.status_id = s.id
|
||||
WHERE m.due_date < CURDATE()
|
||||
AND s.name != 'closed'
|
||||
");
|
||||
$stmt->execute();
|
||||
$overdue = $stmt->fetchAll();
|
||||
|
||||
foreach ($overdue as $task) {
|
||||
if (!empty($task['email'])) {
|
||||
$subject = "تنبيه: مهمة متأخرة! - " . $task['ref_no'];
|
||||
$html = "
|
||||
<div dir='rtl' style='font-family: Arial, sans-serif;'>
|
||||
<h3 style='color: red;'>تنبيه: مهمة متأخرة</h3>
|
||||
<p>عزيزي <b>" . htmlspecialchars($task['full_name']) . "</b>،</p>
|
||||
<p>هذه المهمة قد تجاوزت الموعد النهائي المحدد:</p>
|
||||
<ul>
|
||||
<li><b>رقم المرجع:</b> " . htmlspecialchars($task['ref_no']) . "</li>
|
||||
<li><b>الموضوع:</b> " . htmlspecialchars($task['subject']) . "</li>
|
||||
<li><b>تاريخ الاستحقاق:</b> <span style='color: red;'>" . $task['due_date'] . "</span></li>
|
||||
</ul>
|
||||
<p>يرجى معالجة هذه المهمة في أقرب وقت ممكن.</p>
|
||||
</div>
|
||||
";
|
||||
$res = MailService::sendMail($task['email'], $subject, $html);
|
||||
if ($res['success']) {
|
||||
echo "Sent overdue reminder to " . $task['email'] . " for task " . $task['ref_no'] . PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "[" . date('Y-m-d H:i:s') . "] Reminder process finished." . PHP_EOL;
|
||||
149
stock_dashboard.php
Normal file
149
stock_dashboard.php
Normal file
@ -0,0 +1,149 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
if (!canView('stock_dashboard')) {
|
||||
echo '<div class="alert alert-danger">عذراً، ليس لديك صلاحية الوصول لهذه الصفحة.</div>';
|
||||
require_once __DIR__ . '/includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Stats
|
||||
$total_items = db()->query("SELECT COUNT(*) FROM stock_items")->fetchColumn();
|
||||
$total_stores = db()->query("SELECT COUNT(*) FROM stock_stores")->fetchColumn();
|
||||
$low_stock_count = db()->query("
|
||||
SELECT COUNT(*) FROM stock_quantities q
|
||||
JOIN stock_items i ON q.item_id = i.id
|
||||
WHERE q.quantity <= i.min_quantity
|
||||
")->fetchColumn();
|
||||
|
||||
// Recent Transactions
|
||||
$stmt = db()->query("
|
||||
SELECT t.*, i.name as item_name, s.name as store_name, u.full_name as user_name
|
||||
FROM stock_transactions t
|
||||
JOIN stock_items i ON t.item_id = i.id
|
||||
JOIN stock_stores s ON t.store_id = s.id
|
||||
LEFT JOIN users u ON t.user_id = u.id
|
||||
ORDER BY t.created_at DESC LIMIT 10
|
||||
");
|
||||
$recent_transactions = $stmt->fetchAll();
|
||||
|
||||
?>
|
||||
|
||||
<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">
|
||||
<?php if (canView('stock_in')): ?>
|
||||
<a href="stock_in.php" class="btn btn-sm btn-success me-2">
|
||||
<i class="fas fa-plus"></i> توريد جديد
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if (canView('stock_out')): ?>
|
||||
<a href="stock_out.php" class="btn btn-sm btn-danger">
|
||||
<i class="fas fa-minus"></i> صرف جديد
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0 bg-primary bg-opacity-10 p-3 rounded">
|
||||
<i class="fas fa-box fa-2x text-primary"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<h6 class="text-muted mb-1">إجمالي الأصناف</h6>
|
||||
<h3 class="mb-0 fw-bold"><?= number_format($total_items) ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0 bg-success bg-opacity-10 p-3 rounded">
|
||||
<i class="fas fa-warehouse fa-2x text-success"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<h6 class="text-muted mb-1">المستودعات</h6>
|
||||
<h3 class="mb-0 fw-bold"><?= number_format($total_stores) ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0 bg-warning bg-opacity-10 p-3 rounded">
|
||||
<i class="fas fa-exclamation-triangle fa-2x text-warning"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<h6 class="text-muted mb-1">تنبيهات المخزون المنخفض</h6>
|
||||
<h3 class="mb-0 fw-bold"><?= number_format($low_stock_count) ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0 fw-bold"><i class="fas fa-history me-2 text-secondary"></i> أحدث الحركات</h5>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>النوع</th>
|
||||
<th>الصنف</th>
|
||||
<th>المستودع</th>
|
||||
<th>الكمية</th>
|
||||
<th>بواسطة</th>
|
||||
<th>التاريخ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($recent_transactions)): ?>
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4 text-muted">لا توجد حركات حديثة</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($recent_transactions as $t): ?>
|
||||
<tr>
|
||||
<td><?= $t['id'] ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$badges = [
|
||||
'in' => ['bg-success', 'توريد'],
|
||||
'out' => ['bg-danger', 'صرف'],
|
||||
'damage' => ['bg-dark', 'تالف'],
|
||||
'lend' => ['bg-info text-dark', 'إعارة'],
|
||||
'return' => ['bg-primary', 'إرجاع'],
|
||||
'transfer' => ['bg-secondary', 'نقل']
|
||||
];
|
||||
$b = $badges[$t['transaction_type']] ?? ['bg-secondary', $t['transaction_type']];
|
||||
?>
|
||||
<span class="badge <?= $b[0] ?>"><?= $b[1] ?></span>
|
||||
</td>
|
||||
<td class="fw-bold"><?= htmlspecialchars($t['item_name']) ?></td>
|
||||
<td><?= htmlspecialchars($t['store_name']) ?></td>
|
||||
<td dir="ltr" class="text-end fw-bold"><?= number_format($t['quantity'], 2) ?></td>
|
||||
<td><?= htmlspecialchars($t['user_name'] ?? '-') ?></td>
|
||||
<td><?= date('Y-m-d H:i', strtotime($t['created_at'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
214
stock_in.php
Normal file
214
stock_in.php
Normal file
@ -0,0 +1,214 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
require_once __DIR__ . '/includes/pagination.php';
|
||||
|
||||
if (!canView('stock_in')) {
|
||||
echo '<div class="alert alert-danger">عذراً، ليس لديك صلاحية الوصول لهذه الصفحة.</div>';
|
||||
require_once __DIR__ . '/includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$success = '';
|
||||
$error = '';
|
||||
|
||||
// Handle Form Submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$store_id = $_POST['store_id'] ?? null;
|
||||
$item_id = $_POST['item_id'] ?? null;
|
||||
$quantity = $_POST['quantity'] ?? 0;
|
||||
$reference = $_POST['reference'] ?? '';
|
||||
$notes = $_POST['notes'] ?? '';
|
||||
|
||||
if ($store_id && $item_id && $quantity > 0) {
|
||||
try {
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 1. Create Transaction
|
||||
$stmt = $pdo->prepare("INSERT INTO stock_transactions (transaction_type, store_id, item_id, quantity, user_id, reference, notes) VALUES ('in', ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$store_id, $item_id, $quantity, $_SESSION['user_id'], $reference, $notes]);
|
||||
|
||||
// 2. Update Quantity
|
||||
// Check if record exists
|
||||
$check = $pdo->prepare("SELECT id, quantity FROM stock_quantities WHERE store_id = ? AND item_id = ?");
|
||||
$check->execute([$store_id, $item_id]);
|
||||
$exists = $check->fetch();
|
||||
|
||||
if ($exists) {
|
||||
$new_qty = $exists['quantity'] + $quantity;
|
||||
$update = $pdo->prepare("UPDATE stock_quantities SET quantity = ? WHERE id = ?");
|
||||
$update->execute([$new_qty, $exists['id']]);
|
||||
} else {
|
||||
$insert = $pdo->prepare("INSERT INTO stock_quantities (store_id, item_id, quantity) VALUES (?, ?, ?)");
|
||||
$insert->execute([$store_id, $item_id, $quantity]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
$success = 'تم تسجيل عملية التوريد بنجاح';
|
||||
} catch (PDOException $e) {
|
||||
$pdo->rollBack();
|
||||
$error = 'حدث خطأ: ' . $e->getMessage();
|
||||
}
|
||||
} else {
|
||||
$error = 'يرجى تعبئة جميع الحقول المطلوبة';
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch Data for Dropdowns
|
||||
$stores = db()->query("SELECT * FROM stock_stores ORDER BY name ASC")->fetchAll();
|
||||
$items = db()->query("SELECT * FROM stock_items ORDER BY name ASC")->fetchAll();
|
||||
|
||||
// Pagination for History
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
if ($page < 1) $page = 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$where = "WHERE t.transaction_type = 'in'";
|
||||
$params = [];
|
||||
|
||||
// Count Total
|
||||
$countQuery = "SELECT COUNT(*) FROM stock_transactions t $where";
|
||||
$countStmt = db()->prepare($countQuery);
|
||||
$countStmt->execute($params);
|
||||
$totalFiltered = $countStmt->fetchColumn();
|
||||
|
||||
// Fetch History
|
||||
$historyQuery = "
|
||||
SELECT t.*, i.name as item_name, s.name as store_name, u.full_name as user_name
|
||||
FROM stock_transactions t
|
||||
JOIN stock_items i ON t.item_id = i.id
|
||||
JOIN stock_stores s ON t.store_id = s.id
|
||||
LEFT JOIN users u ON t.user_id = u.id
|
||||
$where
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT $limit OFFSET $offset
|
||||
";
|
||||
$stmt = db()->prepare($historyQuery);
|
||||
$stmt->execute($params);
|
||||
$history = $stmt->fetchAll();
|
||||
|
||||
?>
|
||||
|
||||
<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>
|
||||
<a href="stock_dashboard.php" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-right"></i> عودة للوحة التحكم
|
||||
</a>
|
||||
</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="row justify-content-center mb-5">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0"><i class="fas fa-plus-circle me-2"></i> تسجيل توريد جديد</h5>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<form method="POST">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">المستودع <span class="text-danger">*</span></label>
|
||||
<select name="store_id" class="form-select" required>
|
||||
<option value="">-- اختر المستودع --</option>
|
||||
<?php foreach ($stores as $store): ?>
|
||||
<option value="<?= $store['id'] ?>"><?= htmlspecialchars($store['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">الصنف <span class="text-danger">*</span></label>
|
||||
<select name="item_id" class="form-select" required>
|
||||
<option value="">-- اختر الصنف --</option>
|
||||
<?php foreach ($items as $item): ?>
|
||||
<option value="<?= $item['id'] ?>"><?= htmlspecialchars($item['name']) ?> (<?= htmlspecialchars($item['sku'] ?: '-') ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">الكمية <span class="text-danger">*</span></label>
|
||||
<input type="number" step="0.01" name="quantity" class="form-control" required min="0.01">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">رقم المرجع / الفاتورة</label>
|
||||
<input type="text" name="reference" class="form-control" placeholder="مثال: فاتورة رقم 123">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">ملاحظات</label>
|
||||
<textarea name="notes" class="form-control" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-success btn-lg">
|
||||
<i class="fas fa-save me-2"></i> حفظ عملية التوريد
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History Table -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0 fw-bold"><i class="fas fa-history me-2 text-secondary"></i> سجل عمليات التوريد</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>
|
||||
<th>التاريخ</th>
|
||||
<th>المرجع</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($history)): ?>
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4 text-muted">لا توجد عمليات توريد سابقة.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($history as $h): ?>
|
||||
<tr>
|
||||
<td class="ps-4"><?= $h['id'] ?></td>
|
||||
<td class="fw-bold"><?= htmlspecialchars($h['item_name']) ?></td>
|
||||
<td><?= htmlspecialchars($h['store_name']) ?></td>
|
||||
<td class="text-success fw-bold" dir="ltr">+<?= number_format($h['quantity'], 2) ?></td>
|
||||
<td><?= htmlspecialchars($h['user_name'] ?? '-') ?></td>
|
||||
<td><?= date('Y-m-d H:i', strtotime($h['created_at'])) ?></td>
|
||||
<td><small class="text-muted"><?= htmlspecialchars($h['reference'] ?? '-') ?></small></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php renderPagination($page, $totalFiltered, $limit); ?>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
313
stock_items.php
Normal file
313
stock_items.php
Normal file
@ -0,0 +1,313 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
require_once __DIR__ . '/includes/pagination.php';
|
||||
|
||||
if (!canView('stock_items')) {
|
||||
echo '<div class="alert alert-danger">عذراً، ليس لديك صلاحية الوصول لهذه الصفحة.</div>';
|
||||
require_once __DIR__ . '/includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$success = '';
|
||||
$error = '';
|
||||
|
||||
// Handle Actions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
$id = $_POST['id'] ?? 0;
|
||||
$name = $_POST['name'] ?? '';
|
||||
$sku = $_POST['sku'] ?? '';
|
||||
$category_id = $_POST['category_id'] ?? null;
|
||||
$min_quantity = $_POST['min_quantity'] ?? 0;
|
||||
$unit = $_POST['unit'] ?? 'piece';
|
||||
$description = $_POST['description'] ?? '';
|
||||
|
||||
if ($action === 'add' && canAdd('stock_items')) {
|
||||
if ($name) {
|
||||
$stmt = db()->prepare("INSERT INTO stock_items (name, sku, category_id, min_quantity, unit, description) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
if ($stmt->execute([$name, $sku, $category_id, $min_quantity, $unit, $description])) {
|
||||
$success = 'تم إضافة الصنف بنجاح';
|
||||
} else {
|
||||
$error = 'حدث خطأ أثناء الإضافة';
|
||||
}
|
||||
}
|
||||
} elseif ($action === 'edit' && canEdit('stock_items')) {
|
||||
if ($name && $id) {
|
||||
$stmt = db()->prepare("UPDATE stock_items SET name=?, sku=?, category_id=?, min_quantity=?, unit=?, description=? WHERE id=?");
|
||||
if ($stmt->execute([$name, $sku, $category_id, $min_quantity, $unit, $description, $id])) {
|
||||
$success = 'تم تحديث الصنف بنجاح';
|
||||
} else {
|
||||
$error = 'حدث خطأ أثناء التحديث';
|
||||
}
|
||||
}
|
||||
} elseif ($action === 'delete' && canDelete('stock_items')) {
|
||||
if ($id) {
|
||||
$stmt = db()->prepare("DELETE FROM stock_items WHERE id=?");
|
||||
if ($stmt->execute([$id])) {
|
||||
$success = 'تم حذف الصنف بنجاح';
|
||||
} else {
|
||||
$error = 'لا يمكن حذف الصنف لوجود حركات مرتبطة به';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination & Search
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
if ($page < 1) $page = 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$where = "WHERE 1=1";
|
||||
$params = [];
|
||||
|
||||
if (isset($_GET['search']) && !empty($_GET['search'])) {
|
||||
$where .= " AND (i.name LIKE ? OR i.sku LIKE ?)";
|
||||
$search = "%" . $_GET['search'] . "%";
|
||||
$params = array_merge($params, [$search, $search]);
|
||||
}
|
||||
|
||||
if (isset($_GET['category_id']) && !empty($_GET['category_id'])) {
|
||||
$where .= " AND i.category_id = ?";
|
||||
$params[] = $_GET['category_id'];
|
||||
}
|
||||
|
||||
// Count Total
|
||||
$countQuery = "SELECT COUNT(*) FROM stock_items i $where";
|
||||
$countStmt = db()->prepare($countQuery);
|
||||
$countStmt->execute($params);
|
||||
$totalFiltered = $countStmt->fetchColumn();
|
||||
|
||||
// Fetch Items with Category Name and Total Quantity
|
||||
$query = "
|
||||
SELECT i.*, c.name as category_name,
|
||||
(SELECT SUM(quantity) FROM stock_quantities q WHERE q.item_id = i.id) as total_quantity
|
||||
FROM stock_items i
|
||||
LEFT JOIN stock_categories c ON i.category_id = c.id
|
||||
$where
|
||||
ORDER BY i.name ASC
|
||||
LIMIT $limit OFFSET $offset
|
||||
";
|
||||
$stmt = db()->prepare($query);
|
||||
$stmt->execute($params);
|
||||
$items = $stmt->fetchAll();
|
||||
|
||||
// Fetch Categories for Dropdown
|
||||
$categories = db()->query("SELECT * FROM stock_categories ORDER BY name ASC")->fetchAll();
|
||||
|
||||
?>
|
||||
|
||||
<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('stock_items')): ?>
|
||||
<button type="button" class="btn btn-primary" onclick="openItemModal('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; ?>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-3">
|
||||
<form method="GET" class="row g-2 align-items-center">
|
||||
<div class="col-md-5">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-white border-end-0"><i class="fas fa-search text-muted"></i></span>
|
||||
<input type="text" name="search" class="form-control border-start-0" placeholder="بحث باسم الصنف أو الرمز (SKU)..." value="<?= htmlspecialchars($_GET['search'] ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<select name="category_id" class="form-select" onchange="this.form.submit()">
|
||||
<option value="">جميع التصنيفات</option>
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<option value="<?= $cat['id'] ?>" <?= (isset($_GET['category_id']) && $_GET['category_id'] == $cat['id']) ? 'selected' : '' ?>><?= $cat['name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 text-end">
|
||||
<button type="submit" class="btn btn-light w-100">تصفية</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>الرمز (SKU)</th>
|
||||
<th>التصنيف</th>
|
||||
<th>الكمية الحالية</th>
|
||||
<th>الحد الأدنى</th>
|
||||
<th>الوحدة</th>
|
||||
<th class="text-center">الإجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($items)): ?>
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-5 text-muted">
|
||||
<i class="fas fa-boxes fa-3x mb-3 opacity-20"></i>
|
||||
<p>لا يوجد أصناف مطابقة.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($items as $item): ?>
|
||||
<tr>
|
||||
<td class="ps-4 fw-bold"><?= htmlspecialchars($item['name']) ?></td>
|
||||
<td><?= htmlspecialchars($item['sku'] ?? '-') ?></td>
|
||||
<td><span class="badge bg-secondary"><?= htmlspecialchars($item['category_name'] ?? 'عام') ?></span></td>
|
||||
<td>
|
||||
<?php
|
||||
$qty = $item['total_quantity'] ?: 0;
|
||||
$cls = ($qty <= $item['min_quantity']) ? 'text-danger fw-bold' : 'text-success';
|
||||
?>
|
||||
<span class="<?= $cls ?>"><?= number_format($qty, 2) ?></span>
|
||||
</td>
|
||||
<td><?= number_format($item['min_quantity']) ?></td>
|
||||
<td><?= htmlspecialchars($item['unit']) ?></td>
|
||||
<td class="text-center">
|
||||
<?php if (canEdit('stock_items')): ?>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick='openItemModal("edit", <?= json_encode($item) ?>)'>
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if (canDelete('stock_items')): ?>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="confirmDelete(<?= $item['id'] ?>)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php renderPagination($page, $totalFiltered, $limit); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Item Modal -->
|
||||
<div class="modal fade" id="itemModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-light">
|
||||
<h5 class="modal-title" id="itemModalLabel">إضافة صنف</h5>
|
||||
<button type="button" class="btn-close" 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="">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">اسم الصنف <span class="text-danger">*</span></label>
|
||||
<input type="text" name="name" id="modalName" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">الرمز (SKU)</label>
|
||||
<input type="text" name="sku" id="modalSku" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">التصنيف</label>
|
||||
<select name="category_id" id="modalCategory" class="form-select">
|
||||
<option value="">-- اختر --</option>
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<option value="<?= $cat['id'] ?>"><?= htmlspecialchars($cat['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">الحد الأدنى للكمية</label>
|
||||
<input type="number" name="min_quantity" id="modalMinQty" class="form-control" value="0">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">الوحدة</label>
|
||||
<input type="text" name="unit" id="modalUnit" class="form-control" value="قطعة">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">الوصف</label>
|
||||
<textarea name="description" id="modalDesc" 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 itemModal;
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var modalEl = document.getElementById('itemModal');
|
||||
if (modalEl) {
|
||||
itemModal = new bootstrap.Modal(modalEl);
|
||||
}
|
||||
});
|
||||
|
||||
function openItemModal(action, data = null) {
|
||||
if (!itemModal) return;
|
||||
document.getElementById('modalAction').value = action;
|
||||
document.getElementById('itemModalLabel').textContent = action === 'add' ? 'إضافة صنف جديد' : 'تعديل الصنف';
|
||||
|
||||
if (action === 'edit' && data) {
|
||||
document.getElementById('modalId').value = data.id;
|
||||
document.getElementById('modalName').value = data.name;
|
||||
document.getElementById('modalSku').value = data.sku || '';
|
||||
document.getElementById('modalCategory').value = data.category_id || '';
|
||||
document.getElementById('modalMinQty').value = data.min_quantity;
|
||||
document.getElementById('modalUnit').value = data.unit;
|
||||
document.getElementById('modalDesc').value = data.description || '';
|
||||
} else {
|
||||
document.getElementById('modalId').value = '';
|
||||
document.getElementById('modalName').value = '';
|
||||
document.getElementById('modalSku').value = '';
|
||||
document.getElementById('modalCategory').value = '';
|
||||
document.getElementById('modalMinQty').value = '0';
|
||||
document.getElementById('modalUnit').value = 'قطعة';
|
||||
document.getElementById('modalDesc').value = '';
|
||||
}
|
||||
itemModal.show();
|
||||
}
|
||||
|
||||
function confirmDelete(id) {
|
||||
if(confirm('هل أنت متأكد من حذف هذا الصنف؟')) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.innerHTML = `<input type="hidden" name="action" value="delete"><input type="hidden" name="id" value="${id}">`;
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
251
stock_lending.php
Normal file
251
stock_lending.php
Normal file
@ -0,0 +1,251 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
if (!canView('stock_lending')) {
|
||||
echo '<div class="alert alert-danger">عذراً، ليس لديك صلاحية الوصول لهذه الصفحة.</div>';
|
||||
require_once __DIR__ . '/includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$success = '';
|
||||
$error = '';
|
||||
|
||||
// Handle Actions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'lend') {
|
||||
$store_id = $_POST['store_id'];
|
||||
$item_id = $_POST['item_id'];
|
||||
$quantity = $_POST['quantity'];
|
||||
$borrower = $_POST['borrower_name'];
|
||||
$phone = $_POST['borrower_phone'];
|
||||
$date = $_POST['expected_return_date'];
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Check stock
|
||||
$check = $pdo->prepare("SELECT id, quantity FROM stock_quantities WHERE store_id = ? AND item_id = ? FOR UPDATE");
|
||||
$check->execute([$store_id, $item_id]);
|
||||
$stock = $check->fetch();
|
||||
|
||||
if (!$stock || $stock['quantity'] < $quantity) {
|
||||
throw new Exception("الكمية غير متوفرة للإعارة");
|
||||
}
|
||||
|
||||
// Deduct Stock
|
||||
$pdo->prepare("UPDATE stock_quantities SET quantity = quantity - ? WHERE id = ?")->execute([$quantity, $stock['id']]);
|
||||
|
||||
// Create Transaction
|
||||
$stmt = $pdo->prepare("INSERT INTO stock_transactions (transaction_type, store_id, item_id, quantity, user_id, reference) VALUES ('lend', ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$store_id, $item_id, $quantity, $_SESSION['user_id'], "إعارة: $borrower"]);
|
||||
$trans_id = $pdo->lastInsertId();
|
||||
|
||||
// Create Lending Record
|
||||
$stmt = $pdo->prepare("INSERT INTO stock_lending (transaction_id, borrower_name, borrower_phone, expected_return_date, status) VALUES (?, ?, ?, ?, 'active')");
|
||||
$stmt->execute([$trans_id, $borrower, $phone, $date]);
|
||||
|
||||
$pdo->commit();
|
||||
$success = 'تم تسجيل الإعارة بنجاح';
|
||||
} catch (Exception $e) {
|
||||
$pdo->rollBack();
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
} elseif ($action === 'return') {
|
||||
$lending_id = $_POST['lending_id'];
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Get Lending Info
|
||||
$lend = $pdo->query("SELECT l.*, t.store_id, t.item_id, t.quantity FROM stock_lending l JOIN stock_transactions t ON l.transaction_id = t.id WHERE l.id = $lending_id")->fetch();
|
||||
|
||||
if ($lend && $lend['status'] === 'active') {
|
||||
// Add Stock Back
|
||||
$check = $pdo->prepare("SELECT id FROM stock_quantities WHERE store_id = ? AND item_id = ?");
|
||||
$check->execute([$lend['store_id'], $lend['item_id']]);
|
||||
$s_id = $check->fetchColumn();
|
||||
|
||||
if ($s_id) {
|
||||
$pdo->prepare("UPDATE stock_quantities SET quantity = quantity + ? WHERE id = ?")->execute([$lend['quantity'], $s_id]);
|
||||
} else {
|
||||
$pdo->prepare("INSERT INTO stock_quantities (store_id, item_id, quantity) VALUES (?, ?, ?)")->execute([$lend['store_id'], $lend['item_id'], $lend['quantity']]);
|
||||
}
|
||||
|
||||
// Create Return Transaction
|
||||
$stmt = $pdo->prepare("INSERT INTO stock_transactions (transaction_type, store_id, item_id, quantity, user_id, reference) VALUES ('return', ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$lend['store_id'], $lend['item_id'], $lend['quantity'], $_SESSION['user_id'], "إرجاع إعارة: " . $lend['borrower_name']]);
|
||||
$ret_id = $pdo->lastInsertId();
|
||||
|
||||
// Update Lending Record
|
||||
$pdo->prepare("UPDATE stock_lending SET status = 'returned', return_transaction_id = ? WHERE id = ?")->execute([$ret_id, $lending_id]);
|
||||
|
||||
$pdo->commit();
|
||||
$success = 'تم إرجاع المواد بنجاح';
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$pdo->rollBack();
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch Active Loans
|
||||
$loans = db()->query("
|
||||
SELECT l.*, i.name as item_name, s.name as store_name, t.quantity, t.created_at as lend_date
|
||||
FROM stock_lending l
|
||||
JOIN stock_transactions t ON l.transaction_id = t.id
|
||||
JOIN stock_items i ON t.item_id = i.id
|
||||
JOIN stock_stores s ON t.store_id = s.id
|
||||
WHERE l.status = 'active'
|
||||
ORDER BY l.expected_return_date ASC
|
||||
")->fetchAll();
|
||||
|
||||
$stores = db()->query("SELECT * FROM stock_stores ORDER BY name ASC")->fetchAll();
|
||||
$items = db()->query("SELECT * FROM stock_items ORDER BY name ASC")->fetchAll();
|
||||
|
||||
?>
|
||||
|
||||
<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>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#lendModal">
|
||||
<i class="fas fa-hand-holding me-2"></i> إعارة جديدة
|
||||
</button>
|
||||
</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-header bg-white py-3">
|
||||
<h5 class="mb-0 fw-bold"><i class="fas fa-clock me-2 text-warning"></i> الإعارات النشطة</h5>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>المستعير</th>
|
||||
<th>المادة</th>
|
||||
<th>الكمية</th>
|
||||
<th>تاريخ الإعارة</th>
|
||||
<th>تاريخ الإرجاع المتوقع</th>
|
||||
<th>الحالة</th>
|
||||
<th>الإجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($loans)): ?>
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4 text-muted">لا توجد إعارات نشطة حالياً</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($loans as $loan): ?>
|
||||
<tr>
|
||||
<td class="fw-bold">
|
||||
<?= htmlspecialchars($loan['borrower_name']) ?>
|
||||
<br><small class="text-muted"><?= htmlspecialchars($loan['borrower_phone']) ?></small>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($loan['item_name']) ?></td>
|
||||
<td class="fw-bold"><?= number_format($loan['quantity'], 2) ?></td>
|
||||
<td><?= date('Y-m-d', strtotime($loan['lend_date'])) ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$due = strtotime($loan['expected_return_date']);
|
||||
$cls = ($due < time()) ? 'text-danger fw-bold' : '';
|
||||
?>
|
||||
<span class="<?= $cls ?>"><?= date('Y-m-d', $due) ?></span>
|
||||
</td>
|
||||
<td><span class="badge bg-warning text-dark">نشط</span></td>
|
||||
<td>
|
||||
<form method="POST" onsubmit="return confirm('هل استلمت المواد المرجعة؟ سيتم إضافتها للمخزون.')">
|
||||
<input type="hidden" name="action" value="return">
|
||||
<input type="hidden" name="lending_id" value="<?= $loan['id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-success">
|
||||
<i class="fas fa-check me-1"></i> تسجيل إرجاع
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lend Modal -->
|
||||
<div class="modal fade" id="lendModal" 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">تسجيل إعارة جديدة</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" value="lend">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">المستودع</label>
|
||||
<select name="store_id" class="form-select" required>
|
||||
<?php foreach ($stores as $store): ?>
|
||||
<option value="<?= $store['id'] ?>"><?= htmlspecialchars($store['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">المادة</label>
|
||||
<select name="item_id" class="form-select" required>
|
||||
<?php foreach ($items as $item): ?>
|
||||
<option value="<?= $item['id'] ?>"><?= htmlspecialchars($item['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">الكمية</label>
|
||||
<input type="number" step="0.01" name="quantity" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">اسم المستعير</label>
|
||||
<input type="text" name="borrower_name" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">رقم الهاتف</label>
|
||||
<input type="text" name="borrower_phone" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">تاريخ الإرجاع المتوقع</label>
|
||||
<input type="date" name="expected_return_date" class="form-control" required>
|
||||
</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>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
230
stock_out.php
Normal file
230
stock_out.php
Normal file
@ -0,0 +1,230 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
require_once __DIR__ . '/includes/pagination.php';
|
||||
|
||||
if (!canView('stock_out')) {
|
||||
echo '<div class="alert alert-danger">عذراً، ليس لديك صلاحية الوصول لهذه الصفحة.</div>';
|
||||
require_once __DIR__ . '/includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$success = '';
|
||||
$error = '';
|
||||
|
||||
// Handle Form Submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$store_id = $_POST['store_id'] ?? null;
|
||||
$item_id = $_POST['item_id'] ?? null;
|
||||
$quantity = $_POST['quantity'] ?? 0;
|
||||
$type = $_POST['type'] ?? 'out'; // 'out' or 'damage'
|
||||
$reference = $_POST['reference'] ?? '';
|
||||
$notes = $_POST['notes'] ?? '';
|
||||
|
||||
if ($store_id && $item_id && $quantity > 0) {
|
||||
try {
|
||||
$pdo = db();
|
||||
// Check availability first
|
||||
$check = $pdo->prepare("SELECT id, quantity FROM stock_quantities WHERE store_id = ? AND item_id = ?");
|
||||
$check->execute([$store_id, $item_id]);
|
||||
$stock = $check->fetch();
|
||||
|
||||
if (!$stock || $stock['quantity'] < $quantity) {
|
||||
$error = 'الكمية غير متوفرة في المستودع المحدد. الكمية الحالية: ' . ($stock['quantity'] ?? 0);
|
||||
} else {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 1. Create Transaction
|
||||
$stmt = $pdo->prepare("INSERT INTO stock_transactions (transaction_type, store_id, item_id, quantity, user_id, reference, notes) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$type, $store_id, $item_id, $quantity, $_SESSION['user_id'], $reference, $notes]);
|
||||
|
||||
// 2. Update Quantity
|
||||
$new_qty = $stock['quantity'] - $quantity;
|
||||
$update = $pdo->prepare("UPDATE stock_quantities SET quantity = ? WHERE id = ?");
|
||||
$update->execute([$new_qty, $stock['id']]);
|
||||
|
||||
$pdo->commit();
|
||||
$success = 'تم تسجيل عملية الصرف بنجاح';
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
$error = 'حدث خطأ: ' . $e->getMessage();
|
||||
}
|
||||
} else {
|
||||
$error = 'يرجى تعبئة جميع الحقول المطلوبة';
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch Data for Dropdowns
|
||||
$stores = db()->query("SELECT * FROM stock_stores ORDER BY name ASC")->fetchAll();
|
||||
$items = db()->query("SELECT * FROM stock_items ORDER BY name ASC")->fetchAll();
|
||||
|
||||
// Pagination for History
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
if ($page < 1) $page = 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$where = "WHERE t.transaction_type IN ('out', 'damage')";
|
||||
$params = [];
|
||||
|
||||
// Count Total
|
||||
$countQuery = "SELECT COUNT(*) FROM stock_transactions t $where";
|
||||
$countStmt = db()->prepare($countQuery);
|
||||
$countStmt->execute($params);
|
||||
$totalFiltered = $countStmt->fetchColumn();
|
||||
|
||||
// Fetch History
|
||||
$historyQuery = "
|
||||
SELECT t.*, i.name as item_name, s.name as store_name, u.full_name as user_name
|
||||
FROM stock_transactions t
|
||||
JOIN stock_items i ON t.item_id = i.id
|
||||
JOIN stock_stores s ON t.store_id = s.id
|
||||
LEFT JOIN users u ON t.user_id = u.id
|
||||
$where
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT $limit OFFSET $offset
|
||||
";
|
||||
$stmt = db()->prepare($historyQuery);
|
||||
$stmt->execute($params);
|
||||
$history = $stmt->fetchAll();
|
||||
|
||||
?>
|
||||
|
||||
<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>
|
||||
<a href="stock_dashboard.php" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-right"></i> عودة للوحة التحكم
|
||||
</a>
|
||||
</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="row justify-content-center mb-5">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h5 class="mb-0"><i class="fas fa-minus-circle me-2"></i> تسجيل عملية صرف</h5>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<form method="POST">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">المستودع <span class="text-danger">*</span></label>
|
||||
<select name="store_id" class="form-select" required>
|
||||
<option value="">-- اختر المستودع --</option>
|
||||
<?php foreach ($stores as $store): ?>
|
||||
<option value="<?= $store['id'] ?>"><?= htmlspecialchars($store['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">الصنف <span class="text-danger">*</span></label>
|
||||
<select name="item_id" class="form-select" required>
|
||||
<option value="">-- اختر الصنف --</option>
|
||||
<?php foreach ($items as $item): ?>
|
||||
<option value="<?= $item['id'] ?>"><?= htmlspecialchars($item['name']) ?> (<?= htmlspecialchars($item['sku'] ?: '-') ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">الكمية <span class="text-danger">*</span></label>
|
||||
<input type="number" step="0.01" name="quantity" class="form-control" required min="0.01">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">نوع العملية</label>
|
||||
<select name="type" class="form-select">
|
||||
<option value="out">صرف (استهلاك/بيع)</option>
|
||||
<option value="damage">تالف / منتهي الصلاحية</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">رقم المرجع / للمستلم</label>
|
||||
<input type="text" name="reference" class="form-control" placeholder="مثال: إذن صرف رقم 55">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">ملاحظات</label>
|
||||
<textarea name="notes" class="form-control" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-danger btn-lg">
|
||||
<i class="fas fa-sign-out-alt me-2"></i> تنفيذ الصرف
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History Table -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0 fw-bold"><i class="fas fa-history me-2 text-secondary"></i> سجل عمليات الصرف</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>
|
||||
<th>بواسطة</th>
|
||||
<th>التاريخ</th>
|
||||
<th>المرجع</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($history)): ?>
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-4 text-muted">لا توجد عمليات صرف سابقة.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($history as $h): ?>
|
||||
<tr>
|
||||
<td class="ps-4"><?= $h['id'] ?></td>
|
||||
<td>
|
||||
<?php if ($h['transaction_type'] == 'damage'): ?>
|
||||
<span class="badge bg-dark">تالف</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-danger">صرف</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="fw-bold"><?= htmlspecialchars($h['item_name']) ?></td>
|
||||
<td><?= htmlspecialchars($h['store_name']) ?></td>
|
||||
<td class="text-danger fw-bold" dir="ltr">-<?= number_format($h['quantity'], 2) ?></td>
|
||||
<td><?= htmlspecialchars($h['user_name'] ?? '-') ?></td>
|
||||
<td><?= date('Y-m-d H:i', strtotime($h['created_at'])) ?></td>
|
||||
<td><small class="text-muted"><?= htmlspecialchars($h['reference'] ?? '-') ?></small></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php renderPagination($page, $totalFiltered, $limit); ?>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
173
stock_reports.php
Normal file
173
stock_reports.php
Normal file
@ -0,0 +1,173 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
if (!canView('stock_reports')) {
|
||||
echo '<div class="alert alert-danger">عذراً، ليس لديك صلاحية الوصول لهذه الصفحة.</div>';
|
||||
require_once __DIR__ . '/includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$start_date = $_GET['start_date'] ?? date('Y-m-01');
|
||||
$end_date = $_GET['end_date'] ?? date('Y-m-d');
|
||||
$store_id = $_GET['store_id'] ?? '';
|
||||
$item_id = $_GET['item_id'] ?? '';
|
||||
$type = $_GET['type'] ?? '';
|
||||
|
||||
// Build Query
|
||||
$sql = "
|
||||
SELECT t.*, i.name as item_name, s.name as store_name, u.full_name as user_name
|
||||
FROM stock_transactions t
|
||||
JOIN stock_items i ON t.item_id = i.id
|
||||
JOIN stock_stores s ON t.store_id = s.id
|
||||
LEFT JOIN users u ON t.user_id = u.id
|
||||
WHERE DATE(t.created_at) BETWEEN ? AND ?
|
||||
";
|
||||
$params = [$start_date, $end_date];
|
||||
|
||||
if ($store_id) {
|
||||
$sql .= " AND t.store_id = ?";
|
||||
$params[] = $store_id;
|
||||
}
|
||||
if ($item_id) {
|
||||
$sql .= " AND t.item_id = ?";
|
||||
$params[] = $item_id;
|
||||
}
|
||||
if ($type) {
|
||||
$sql .= " AND t.transaction_type = ?";
|
||||
$params[] = $type;
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY t.created_at DESC";
|
||||
|
||||
$stmt = db()->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$transactions = $stmt->fetchAll();
|
||||
|
||||
$stores = db()->query("SELECT * FROM stock_stores ORDER BY name ASC")->fetchAll();
|
||||
$items = db()->query("SELECT * FROM stock_items ORDER BY name ASC")->fetchAll();
|
||||
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom d-print-none">
|
||||
<h1 class="h2">تقارير حركة المخزون</h1>
|
||||
<button onclick="window.print()" class="btn btn-secondary">
|
||||
<i class="fas fa-print me-2"></i> طباعة التقرير
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 mb-4 d-print-none">
|
||||
<div class="card-body bg-light">
|
||||
<form method="GET" class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold">من تاريخ</label>
|
||||
<input type="date" name="start_date" class="form-control" value="<?= $start_date ?>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold">إلى تاريخ</label>
|
||||
<input type="date" name="end_date" class="form-control" value="<?= $end_date ?>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label fw-bold">المستودع</label>
|
||||
<select name="store_id" class="form-select">
|
||||
<option value="">الكل</option>
|
||||
<?php foreach ($stores as $s): ?>
|
||||
<option value="<?= $s['id'] ?>" <?= $store_id == $s['id'] ? 'selected' : '' ?>><?= htmlspecialchars($s['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label fw-bold">الصنف</label>
|
||||
<select name="item_id" class="form-select">
|
||||
<option value="">الكل</option>
|
||||
<?php foreach ($items as $i): ?>
|
||||
<option value="<?= $i['id'] ?>" <?= $item_id == $i['id'] ? 'selected' : '' ?>><?= htmlspecialchars($i['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="fas fa-filter me-2"></i> عرض
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-none d-print-block text-center mb-4">
|
||||
<h3>تقرير حركة المخزون</h3>
|
||||
<p>من <?= $start_date ?> إلى <?= $end_date ?></p>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>التاريخ</th>
|
||||
<th>النوع</th>
|
||||
<th>المستودع</th>
|
||||
<th>الصنف</th>
|
||||
<th>الكمية</th>
|
||||
<th>المستخدم</th>
|
||||
<th>ملاحظات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($transactions)): ?>
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-4 text-muted">لا توجد بيانات للعرض</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($transactions as $t): ?>
|
||||
<tr>
|
||||
<td><?= $t['id'] ?></td>
|
||||
<td><?= date('Y-m-d H:i', strtotime($t['created_at'])) ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$types = [
|
||||
'in' => 'توريد',
|
||||
'out' => 'صرف',
|
||||
'damage' => 'تالف',
|
||||
'lend' => 'إعارة',
|
||||
'return' => 'إرجاع',
|
||||
'transfer' => 'نقل'
|
||||
];
|
||||
echo $types[$t['transaction_type']] ?? $t['transaction_type'];
|
||||
?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($t['store_name']) ?></td>
|
||||
<td class="fw-bold"><?= htmlspecialchars($t['item_name']) ?></td>
|
||||
<td dir="ltr" class="text-end fw-bold">
|
||||
<?php if ($t['transaction_type'] == 'in' || $t['transaction_type'] == 'return'): ?>
|
||||
<span class="text-success">+<?= number_format($t['quantity'], 2) ?></span>
|
||||
<?php else: ?>
|
||||
<span class="text-danger">-<?= number_format($t['quantity'], 2) ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($t['user_name'] ?? '-') ?></td>
|
||||
<td class="small text-muted">
|
||||
<?php if ($t['reference']): ?>
|
||||
<strong>المرجع:</strong> <?= htmlspecialchars($t['reference']) ?><br>
|
||||
<?php endif; ?>
|
||||
<?= htmlspecialchars($t['notes'] ?? '') ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@media print {
|
||||
.btn, .sidebar, .top-navbar, form { display: none !important; }
|
||||
.main-content { margin: 0 !important; padding: 0 !important; }
|
||||
.card { border: none !important; box-shadow: none !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
265
stock_settings.php
Normal file
265
stock_settings.php
Normal file
@ -0,0 +1,265 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
if (!canView('stock_settings')) {
|
||||
echo '<div class="alert alert-danger">عذراً، ليس لديك صلاحية الوصول لهذه الصفحة.</div>';
|
||||
require_once __DIR__ . '/includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$success = '';
|
||||
$error = '';
|
||||
|
||||
// Handle Actions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
$type = $_POST['type'] ?? ''; // 'store' or 'category'
|
||||
$id = $_POST['id'] ?? 0;
|
||||
$name = $_POST['name'] ?? '';
|
||||
$desc = $_POST['description'] ?? ''; // location or description
|
||||
|
||||
if ($action === 'add' && canAdd('stock_settings')) {
|
||||
if ($name) {
|
||||
if ($type === 'store') {
|
||||
$stmt = db()->prepare("INSERT INTO stock_stores (name, location) VALUES (?, ?)");
|
||||
$stmt->execute([$name, $desc]);
|
||||
} else {
|
||||
$stmt = db()->prepare("INSERT INTO stock_categories (name, description) VALUES (?, ?)");
|
||||
$stmt->execute([$name, $desc]);
|
||||
}
|
||||
$success = 'تم الإضافة بنجاح';
|
||||
}
|
||||
} elseif ($action === 'edit' && canEdit('stock_settings')) {
|
||||
if ($name && $id) {
|
||||
if ($type === 'store') {
|
||||
$stmt = db()->prepare("UPDATE stock_stores SET name=?, location=? WHERE id=?");
|
||||
$stmt->execute([$name, $desc, $id]);
|
||||
} else {
|
||||
$stmt = db()->prepare("UPDATE stock_categories SET name=?, description=? WHERE id=?");
|
||||
$stmt->execute([$name, $desc, $id]);
|
||||
}
|
||||
$success = 'تم التحديث بنجاح';
|
||||
}
|
||||
} elseif ($action === 'delete' && canDelete('stock_settings')) {
|
||||
if ($id) {
|
||||
try {
|
||||
if ($type === 'store') {
|
||||
$stmt = db()->prepare("DELETE FROM stock_stores WHERE id=?");
|
||||
$stmt->execute([$id]);
|
||||
} else {
|
||||
$stmt = db()->prepare("DELETE FROM stock_categories WHERE id=?");
|
||||
$stmt->execute([$id]);
|
||||
}
|
||||
$success = 'تم الحذف بنجاح';
|
||||
} catch (PDOException $e) {
|
||||
$error = 'لا يمكن الحذف لوجود بيانات مرتبطة';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$stores = db()->query("SELECT * FROM stock_stores ORDER BY name ASC")->fetchAll();
|
||||
$categories = db()->query("SELECT * FROM stock_categories ORDER BY name ASC")->fetchAll();
|
||||
|
||||
?>
|
||||
|
||||
<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>
|
||||
|
||||
<?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; ?>
|
||||
|
||||
<ul class="nav nav-tabs mb-4" id="stockSettingsTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="stores-tab" data-bs-toggle="tab" data-bs-target="#stores" type="button" role="tab">المستودعات</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="categories-tab" data-bs-toggle="tab" data-bs-target="#categories" type="button" role="tab">التصنيفات</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="stockSettingsContent">
|
||||
<!-- Stores Tab -->
|
||||
<div class="tab-pane fade show active" id="stores" role="tabpanel">
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<h5 class="fw-bold">قائمة المستودعات</h5>
|
||||
<?php if (canAdd('stock_settings')): ?>
|
||||
<button class="btn btn-primary btn-sm" onclick="openModal('store', 'add')">
|
||||
<i class="fas fa-plus"></i> إضافة مستودع
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>اسم المستودع</th>
|
||||
<th>الموقع / العنوان</th>
|
||||
<th>تاريخ الإضافة</th>
|
||||
<th>الإجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($stores as $s): ?>
|
||||
<tr>
|
||||
<td><?= $s['id'] ?></td>
|
||||
<td class="fw-bold"><?= htmlspecialchars($s['name']) ?></td>
|
||||
<td><?= htmlspecialchars($s['location'] ?? '-') ?></td>
|
||||
<td><?= date('Y-m-d', strtotime($s['created_at'])) ?></td>
|
||||
<td>
|
||||
<?php if (canEdit('stock_settings')): ?>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick='openModal("store", "edit", <?= json_encode($s) ?>)'>
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if (canDelete('stock_settings')): ?>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="confirmDelete('store', <?= $s['id'] ?>)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Categories Tab -->
|
||||
<div class="tab-pane fade" id="categories" role="tabpanel">
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<h5 class="fw-bold">تصنيفات المواد</h5>
|
||||
<?php if (canAdd('stock_settings')): ?>
|
||||
<button class="btn btn-primary btn-sm" onclick="openModal('category', 'add')">
|
||||
<i class="fas fa-plus"></i> إضافة تصنيف
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>اسم التصنيف</th>
|
||||
<th>الوصف</th>
|
||||
<th>تاريخ الإضافة</th>
|
||||
<th>الإجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($categories as $c): ?>
|
||||
<tr>
|
||||
<td><?= $c['id'] ?></td>
|
||||
<td class="fw-bold"><?= htmlspecialchars($c['name']) ?></td>
|
||||
<td><?= htmlspecialchars($c['description'] ?? '-') ?></td>
|
||||
<td><?= date('Y-m-d', strtotime($c['created_at'])) ?></td>
|
||||
<td>
|
||||
<?php if (canEdit('stock_settings')): ?>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick='openModal("category", "edit", <?= json_encode($c) ?>)'>
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if (canDelete('stock_settings')): ?>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="confirmDelete('category', <?= $c['id'] ?>)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generic Modal -->
|
||||
<div class="modal fade" id="settingsModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-light">
|
||||
<h5 class="modal-title" id="modalTitle">إضافة</h5>
|
||||
<button type="button" class="btn-close" 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="type" id="modalType" value="">
|
||||
<input type="hidden" name="id" id="modalId" value="">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">الاسم <span class="text-danger">*</span></label>
|
||||
<input type="text" name="name" id="modalName" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold" id="descLabel">الوصف / الموقع</label>
|
||||
<textarea name="description" id="modalDesc" 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 settingsModal;
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
settingsModal = new bootstrap.Modal(document.getElementById('settingsModal'));
|
||||
});
|
||||
|
||||
function openModal(type, action, data = null) {
|
||||
document.getElementById('modalType').value = type;
|
||||
document.getElementById('modalAction').value = action;
|
||||
|
||||
const typeName = type === 'store' ? 'مستودع' : 'تصنيف';
|
||||
document.getElementById('modalTitle').textContent = (action === 'add' ? 'إضافة ' : 'تعديل ') + typeName;
|
||||
document.getElementById('descLabel').textContent = type === 'store' ? 'الموقع / العنوان' : 'الوصف';
|
||||
|
||||
if (action === 'edit' && data) {
|
||||
document.getElementById('modalId').value = data.id;
|
||||
document.getElementById('modalName').value = data.name;
|
||||
document.getElementById('modalDesc').value = (type === 'store' ? data.location : data.description) || '';
|
||||
} else {
|
||||
document.getElementById('modalId').value = '';
|
||||
document.getElementById('modalName').value = '';
|
||||
document.getElementById('modalDesc').value = '';
|
||||
}
|
||||
settingsModal.show();
|
||||
}
|
||||
|
||||
function confirmDelete(type, id) {
|
||||
if(confirm('هل أنت متأكد من الحذف؟')) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.innerHTML = `<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="type" value="${type}">
|
||||
<input type="hidden" name="id" value="${id}">`;
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
1
test_install.php
Normal file
1
test_install.php
Normal file
@ -0,0 +1 @@
|
||||
<?php echo "Test file works!"; ?>
|
||||
50
trial_balance.php
Normal file
50
trial_balance.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
require_once 'includes/header.php';
|
||||
require_once 'includes/accounting_functions.php';
|
||||
|
||||
// Check permission
|
||||
if (!canView('accounting')) {
|
||||
echo "<div class='container mt-4' dir='rtl'>لا تملك صلاحية الوصول لهذه الصفحة.</div>";
|
||||
require_once 'includes/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$trial_balance = get_trial_balance();
|
||||
?>
|
||||
|
||||
<div class="container mt-4" dir="rtl">
|
||||
<h2 class="text-right">ميزان المراجعة (Trial Balance)</h2>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<table class="table table-bordered text-right">
|
||||
<thead><tr><th>الحساب</th><th>مدين</th><th>دائن</th></tr></thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$total_debit = 0;
|
||||
$total_credit = 0;
|
||||
foreach ($trial_balance as $row):
|
||||
$total_debit += $row['total_debit'];
|
||||
$total_credit += $row['total_credit'];
|
||||
?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($row['account_name']) ?></td>
|
||||
<td><?= number_format($row['total_debit'], 2) ?></td>
|
||||
<td><?= number_format($row['total_credit'], 2) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="fw-bold bg-light">
|
||||
<td>الإجمالي</td>
|
||||
<td><?= number_format($total_debit, 2) ?></td>
|
||||
<td><?= number_format($total_credit, 2) ?></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
BIN
uploads/attachments/1772212176_Invoice_8.pdf
Normal file
BIN
uploads/attachments/1772212176_Invoice_8.pdf
Normal file
Binary file not shown.
BIN
uploads/attachments/1772212948_Invoice_8 (1).pdf
Normal file
BIN
uploads/attachments/1772212948_Invoice_8 (1).pdf
Normal file
Binary file not shown.
BIN
uploads/charity/favicon_1772214262.jpg
Normal file
BIN
uploads/charity/favicon_1772214262.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
uploads/charity/favicon_1772214275.jpg
Normal file
BIN
uploads/charity/favicon_1772214275.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
uploads/charity/logo_1772214243.jpg
Normal file
BIN
uploads/charity/logo_1772214243.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
324
user_dashboard.php
Normal file
324
user_dashboard.php
Normal file
@ -0,0 +1,324 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Check if user has view permission
|
||||
if (!isLoggedIn()) {
|
||||
redirect('login.php');
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$user_role = $_SESSION['user_role'];
|
||||
$is_admin = isAdmin();
|
||||
$is_clerk = ($user_role === 'clerk');
|
||||
|
||||
// Stats for this specific user - Combine from all tables
|
||||
$my_total_assignments = 0;
|
||||
$my_pending_tasks = 0;
|
||||
|
||||
foreach (['inbound', 'outbound', 'internal'] as $t) {
|
||||
if (canView($t)) {
|
||||
$table = $t . '_mail';
|
||||
$stmt = db()->prepare("SELECT COUNT(*) FROM $table WHERE assigned_to = ?");
|
||||
$stmt->execute([$user_id]);
|
||||
$my_total_assignments += $stmt->fetchColumn();
|
||||
|
||||
$stmt = db()->prepare("SELECT COUNT(*) FROM $table WHERE assigned_to = ? AND status_id IN (SELECT id FROM mailbox_statuses WHERE name != 'closed')");
|
||||
$stmt->execute([$user_id]);
|
||||
$my_pending_tasks += $stmt->fetchColumn();
|
||||
}
|
||||
}
|
||||
|
||||
// Global Stats
|
||||
$total_inbound = canView('inbound') ? db()->query("SELECT COUNT(*) FROM inbound_mail")->fetchColumn() : 0;
|
||||
$total_outbound = canView('outbound') ? db()->query("SELECT COUNT(*) FROM outbound_mail")->fetchColumn() : 0;
|
||||
|
||||
// Fetch statuses for badge and count
|
||||
$statuses_data = db()->query("SELECT * FROM mailbox_statuses")->fetchAll(PDO::FETCH_UNIQUE);
|
||||
|
||||
// My Assignments
|
||||
$my_assignments = [];
|
||||
$assignment_queries = [];
|
||||
if (canView('inbound')) $assignment_queries[] = "SELECT id, 'inbound' as type, ref_no, subject, due_date, status_id, created_at FROM inbound_mail WHERE assigned_to = $user_id";
|
||||
if (canView('outbound')) $assignment_queries[] = "SELECT id, 'outbound' as type, ref_no, subject, due_date, status_id, created_at FROM outbound_mail WHERE assigned_to = $user_id";
|
||||
if (canView('internal')) $assignment_queries[] = "SELECT id, 'internal' as type, ref_no, subject, due_date, status_id, created_at FROM internal_mail WHERE assigned_to = $user_id";
|
||||
|
||||
if (!empty($assignment_queries)) {
|
||||
$full_assignment_query = "(" . implode(") UNION ALL (", $assignment_queries) . ") ORDER BY created_at DESC LIMIT 10";
|
||||
$stmt = db()->query($full_assignment_query);
|
||||
$my_assignments = $stmt->fetchAll();
|
||||
foreach ($my_assignments as &$m) {
|
||||
$m['status_name'] = $statuses_data[$m['status_id']]['name'] ?? 'unknown';
|
||||
$m['status_color'] = $statuses_data[$m['status_id']]['color'] ?? '#6c757d';
|
||||
}
|
||||
}
|
||||
|
||||
// Recent Activity
|
||||
$recent_activity = [];
|
||||
$recent_queries = [];
|
||||
if (canView('inbound')) $recent_queries[] = "SELECT id, 'inbound' as type, ref_no, subject, status_id, created_by, assigned_to, updated_at FROM inbound_mail";
|
||||
if (canView('outbound')) $recent_queries[] = "SELECT id, 'outbound' as type, ref_no, subject, status_id, created_by, assigned_to, updated_at FROM outbound_mail";
|
||||
if (canView('internal')) $recent_queries[] = "SELECT id, 'internal' as type, ref_no, subject, status_id, created_by, assigned_to, updated_at FROM internal_mail";
|
||||
|
||||
if (!empty($recent_queries)) {
|
||||
$full_recent_query = "(" . implode(") UNION ALL (", $recent_queries) . ")";
|
||||
|
||||
if ($is_admin || $is_clerk) {
|
||||
$full_recent_query = "SELECT * FROM ($full_recent_query) AS combined WHERE (type != 'internal' OR assigned_to = $user_id OR created_by = $user_id) ORDER BY updated_at DESC LIMIT 10";
|
||||
} else {
|
||||
$full_recent_query = "SELECT * FROM ($full_recent_query) AS combined WHERE (assigned_to = $user_id OR created_by = $user_id) ORDER BY updated_at DESC LIMIT 10";
|
||||
}
|
||||
|
||||
$stmt = db()->query($full_recent_query);
|
||||
$recent_activity = $stmt->fetchAll();
|
||||
foreach ($recent_activity as &$a) {
|
||||
$a['status_name'] = $statuses_data[$a['status_id']]['name'] ?? 'unknown';
|
||||
$a['status_color'] = $statuses_data[$a['status_id']]['color'] ?? '#6c757d';
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusBadge($mail) {
|
||||
$status_name = $mail['status_name'] ?? 'غير معروف';
|
||||
$status_color = $mail['status_color'] ?? '#6c757d';
|
||||
|
||||
$display_name = $status_name;
|
||||
if ($status_name == 'received') $display_name = 'تم الاستلام';
|
||||
if ($status_name == 'in_progress') $display_name = 'قيد المعالجة';
|
||||
if ($status_name == 'closed') $display_name = 'مكتمل';
|
||||
|
||||
return '<span class="badge" style="background-color: ' . $status_color . ';">' . htmlspecialchars($display_name) . '</span>';
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card bg-dark text-white p-4 shadow-lg border-0 overflow-hidden position-relative">
|
||||
<div class="position-absolute end-0 top-0 p-3 opacity-10">
|
||||
<i class="fas fa-envelope-open-text fa-10x" style="transform: rotate(-15deg);"></i>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center position-relative">
|
||||
<div>
|
||||
<h2 class="fw-bold mb-1">مرحباً، <?= htmlspecialchars($current_user['full_name'] ?? $_SESSION['name']) ?>!</h2>
|
||||
<p class="mb-0 opacity-75">
|
||||
أنت مسجل كـ <strong>
|
||||
<?php
|
||||
if ($is_admin) echo 'مدير النظام';
|
||||
elseif ($is_clerk) echo 'كاتب';
|
||||
else echo 'موظف';
|
||||
?>
|
||||
</strong>.
|
||||
<?php if ($is_admin || $is_clerk): ?>
|
||||
يمكنك متابعة كافة المراسلات وإدارة المهام.
|
||||
<?php else: ?>
|
||||
تابع مهامك المسندة إليك هنا.
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-none d-md-block">
|
||||
<?php if ($current_user['profile_image']): ?>
|
||||
<img src="<?= $current_user['profile_image'] ?>?v=<?= time() ?>" alt="Profile" class="rounded-circle border border-3 border-white shadow" style="width: 100px; height: 100px; object-fit: cover;">
|
||||
<?php else: ?>
|
||||
<div class="bg-white bg-opacity-25 rounded-circle d-flex align-items-center justify-content-center border border-3 border-white shadow" style="width: 100px; height: 100px;">
|
||||
<i class="fas fa-user fa-3x text-white"></i>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 p-3 shadow-sm border-0 border-start border-primary border-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="bg-primary bg-opacity-10 p-3 rounded-3 me-3">
|
||||
<i class="fas fa-tasks text-primary fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="text-muted mb-1">مهامي</h6>
|
||||
<h3 class="fw-bold mb-0"><?= $my_total_assignments ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 p-3 shadow-sm border-0 border-start border-warning border-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="bg-warning bg-opacity-10 p-3 rounded-3 me-3">
|
||||
<i class="fas fa-clock text-warning fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="text-muted mb-1">قيد التنفيذ</h6>
|
||||
<h3 class="fw-bold mb-0"><?= $my_pending_tasks ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($is_admin || $is_clerk): ?>
|
||||
<?php if (canView('inbound')): ?>
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 p-3 shadow-sm border-0 border-start border-info border-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="bg-info bg-opacity-10 p-3 rounded-3 me-3">
|
||||
<i class="fas fa-download text-info fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="text-muted mb-1">إجمالي الوارد</h6>
|
||||
<h3 class="fw-bold mb-0"><?= $total_inbound ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (canView('outbound')): ?>
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 p-3 shadow-sm border-0 border-start border-success border-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="bg-success bg-opacity-10 p-3 rounded-3 me-3">
|
||||
<i class="fas fa-upload text-success fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="text-muted mb-1">إجمالي الصادر</h6>
|
||||
<h3 class="fw-bold mb-0"><?= $total_outbound ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 p-3 shadow-sm border-0 border-start border-info border-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="bg-info bg-opacity-10 p-3 rounded-3 me-3">
|
||||
<i class="fas fa-envelope-open text-info fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="text-muted mb-1">وارد من قبلي</h6>
|
||||
<?php
|
||||
$my_in_count = db()->prepare("SELECT COUNT(*) FROM inbound_mail WHERE created_by = ?");
|
||||
$my_in_count->execute([$user_id]);
|
||||
$my_in_count = $my_in_count->fetchColumn();
|
||||
?>
|
||||
<h3 class="fw-bold mb-0"><?= $my_in_count ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 p-3 shadow-sm border-0 border-start border-success border-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="bg-success bg-opacity-10 p-3 rounded-3 me-3">
|
||||
<i class="fas fa-paper-plane text-success fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="text-muted mb-1">صادر من قبلي</h6>
|
||||
<?php
|
||||
$my_out_count = db()->prepare("SELECT COUNT(*) FROM outbound_mail WHERE created_by = ?");
|
||||
$my_out_count->execute([$user_id]);
|
||||
$my_out_count = $my_out_count->fetchColumn();
|
||||
?>
|
||||
<h3 class="fw-bold mb-0"><?= $my_out_count ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm border-0 mb-4 h-100">
|
||||
<div class="card-header bg-white py-3 border-bottom d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 fw-bold"><i class="fas fa-clipboard-list me-2 text-primary"></i> مهامي المسندة</h5>
|
||||
<div class="btn-group">
|
||||
<?php if (canAdd('inbound')): ?>
|
||||
<a href="inbound.php?action=add" class="btn btn-sm btn-outline-primary">إضافة وارد</a>
|
||||
<?php endif; ?>
|
||||
<?php if (canAdd('outbound')): ?>
|
||||
<a href="outbound.php?action=add" class="btn btn-sm btn-outline-success">إضافة صادر</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<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 class="pe-4 text-center">الإجراء</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($my_assignments)): ?>
|
||||
<?php foreach ($my_assignments as $mail): ?>
|
||||
<tr style="cursor: pointer;" onclick="window.location='view_mail.php?id=<?= $mail['id'] ?>&type=<?= $mail['type'] ?>'">
|
||||
<td class="ps-4 fw-bold text-primary"><?= $mail['ref_no'] ?></td>
|
||||
<td><?= htmlspecialchars($mail['subject']) ?></td>
|
||||
<td>
|
||||
<?php if ($mail['due_date']): ?>
|
||||
<small class="<?= (strtotime($mail['due_date']) < time() && $mail['status_name'] != 'closed') ? 'text-danger fw-bold' : 'text-muted' ?>">
|
||||
<?= $mail['due_date'] ?>
|
||||
</small>
|
||||
<?php else: ?>
|
||||
<small class="text-muted">-</small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= getStatusBadge($mail) ?></td>
|
||||
<td class="pe-4 text-center">
|
||||
<a href="view_mail.php?id=<?= $mail['id'] ?>&type=<?= $mail['type'] ?>" class="btn btn-sm btn-light rounded-pill px-3">عرض</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5 text-muted">
|
||||
أنت على اطلاع بكافة مهامك! لا توجد مهام معلقة.
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm border-0 mb-4 h-100">
|
||||
<div class="card-header bg-white py-3 border-bottom">
|
||||
<h5 class="mb-0 fw-bold"><i class="fas fa-bell me-2 text-warning"></i> <?= ($is_admin || $is_clerk) ? 'آخر المراسلات' : 'نشاطاتي الأخيرة' ?></h5>
|
||||
</div>
|
||||
<div class="card-body p-0" style="max-height: 500px; overflow-y: auto;">
|
||||
<div class="list-group list-group-flush">
|
||||
<?php if (!empty($recent_activity)): ?>
|
||||
<?php foreach ($recent_activity as $act): ?>
|
||||
<a href="view_mail.php?id=<?= $act['id'] ?>&type=<?= $act['type'] ?>" class="list-group-item list-group-item-action p-3 border-0 border-bottom">
|
||||
<div class="d-flex w-100 justify-content-between mb-1">
|
||||
<h6 class="mb-1 fw-bold text-truncate" title="<?= htmlspecialchars($act['subject']) ?>"><?= htmlspecialchars($act['subject']) ?></h6>
|
||||
<small class="text-muted"><?= date('m-d', strtotime($act['updated_at'])) ?></small>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted">
|
||||
<i class="fas <?= $act['type'] == 'inbound' ? 'fa-arrow-down text-primary' : ($act['type'] == 'outbound' ? 'fa-arrow-up text-success' : 'fa-exchange-alt text-info') ?> me-1"></i>
|
||||
<?= $act['ref_no'] ?>
|
||||
</small>
|
||||
<?= getStatusBadge($act) ?>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<div class="text-center py-5 text-muted">
|
||||
لا يوجد نشاط مسجل
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
537
users.php
Normal file
537
users.php
Normal file
@ -0,0 +1,537 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
require_once __DIR__ . '/includes/pagination.php';
|
||||
|
||||
if (!canView('users')) {
|
||||
redirect('index.php');
|
||||
}
|
||||
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
$modules = [
|
||||
'inbound' => 'البريد الوارد',
|
||||
'outbound' => 'البريد الصادر',
|
||||
'internal' => 'البريد الداخلي',
|
||||
'users' => 'إدارة المستخدمين',
|
||||
'settings' => 'الإعدادات',
|
||||
'reports' => 'التقارير',
|
||||
'accounting' => 'المحاسبة',
|
||||
'hr_dashboard' => 'HR - لوحة التحكم',
|
||||
'hr_employees' => 'HR - الموظفين',
|
||||
'hr_attendance' => 'HR - الحضور والعطلات',
|
||||
'hr_leaves' => 'HR - الإجازات',
|
||||
'hr_payroll' => 'HR - الرواتب',
|
||||
'hr_reports' => 'HR - التقارير',
|
||||
'stock_dashboard' => 'المخزون - لوحة التحكم',
|
||||
'stock_items' => 'المخزون - الأصناف',
|
||||
'stock_in' => 'المخزون - توريد (وارد)',
|
||||
'stock_out' => 'المخزون - صرف (صادر)',
|
||||
'stock_lending' => 'المخزون - الإعارة',
|
||||
'stock_reports' => 'المخزون - التقارير',
|
||||
'stock_settings' => 'المخزون - الإعدادات',
|
||||
'expenses' => 'المصروفات',
|
||||
'expense_settings' => 'المصروفات - الإعدادات',
|
||||
'meetings' => 'الاجتماعات'
|
||||
];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
$username = $_POST['username'] ?? '';
|
||||
$full_name = $_POST['full_name'] ?? '';
|
||||
$email = $_POST['email'] ?? '';
|
||||
$role = $_POST['role'] ?? 'staff';
|
||||
$password = $_POST['password'] ?? '';
|
||||
$id = $_POST['id'] ?? 0;
|
||||
|
||||
// Global permissions (legacy/fallback)
|
||||
$can_view = isset($_POST['can_view_global']) ? 1 : 0;
|
||||
$can_add = isset($_POST['can_add_global']) ? 1 : 0;
|
||||
$can_edit = isset($_POST['can_edit_global']) ? 1 : 0;
|
||||
$can_delete = isset($_POST['can_delete_global']) ? 1 : 0;
|
||||
|
||||
// Handle Profile Image Upload
|
||||
$profile_image = null;
|
||||
if ($id > 0) {
|
||||
$stmt = db()->prepare("SELECT profile_image FROM users WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$profile_image = $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
if (isset($_FILES['profile_image']) && $_FILES['profile_image']['error'] === UPLOAD_ERR_OK) {
|
||||
$upload_dir = 'uploads/profiles/';
|
||||
if (!is_dir($upload_dir)) mkdir($upload_dir, 0775, true);
|
||||
|
||||
$file_ext = pathinfo($_FILES['profile_image']['name'], PATHINFO_EXTENSION);
|
||||
$new_file_name = time() . '_u' . ($id ?: 'new') . '.' . $file_ext;
|
||||
$target_file = $upload_dir . $new_file_name;
|
||||
|
||||
if (move_uploaded_file($_FILES['profile_image']['tmp_name'], $target_file)) {
|
||||
$profile_image = $target_file;
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'add') {
|
||||
if (!canAdd('users')) redirect('users.php');
|
||||
if ($username && $password && $full_name) {
|
||||
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
|
||||
try {
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO users (username, password, full_name, email, role, profile_image, can_view, can_add, can_edit, can_delete) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$username, $hashed_password, $full_name, $email, $role, $profile_image, $can_view, $can_add, $can_edit, $can_delete]);
|
||||
$user_id = $pdo->lastInsertId();
|
||||
|
||||
// Save page permissions
|
||||
$perm_stmt = $pdo->prepare("INSERT INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
foreach ($modules as $module => $label) {
|
||||
$m_view = isset($_POST["perm_{$module}_view"]) ? 1 : 0;
|
||||
$m_add = isset($_POST["perm_{$module}_add"]) ? 1 : 0;
|
||||
$m_edit = isset($_POST["perm_{$module}_edit"]) ? 1 : 0;
|
||||
$m_delete = isset($_POST["perm_{$module}_delete"]) ? 1 : 0;
|
||||
$perm_stmt->execute([$user_id, $module, $m_view, $m_add, $m_edit, $m_delete]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
$_SESSION['success'] = 'تم إضافة المستخدم بنجاح';
|
||||
redirect('users.php');
|
||||
} catch (PDOException $e) {
|
||||
if (isset($pdo)) $pdo->rollBack();
|
||||
if ($e->getCode() == 23000) {
|
||||
$error = 'اسم المستخدم موجود مسبقاً';
|
||||
} else {
|
||||
$error = 'حدث خطأ: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$error = 'يرجى ملء جميع الحقول المطلوبة';
|
||||
}
|
||||
} elseif ($action === 'edit') {
|
||||
if (!canEdit('users')) redirect('users.php');
|
||||
if ($username && $full_name && $id) {
|
||||
try {
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
if ($password) {
|
||||
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
|
||||
$stmt = $pdo->prepare("UPDATE users SET username = ?, full_name = ?, email = ?, role = ?, profile_image = ?, password = ?, can_view = ?, can_add = ?, can_edit = ?, can_delete = ? WHERE id = ?");
|
||||
$stmt->execute([$username, $full_name, $email, $role, $profile_image, $hashed_password, $can_view, $can_add, $can_edit, $can_delete, $id]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("UPDATE users SET username = ?, full_name = ?, email = ?, role = ?, profile_image = ?, can_view = ?, can_add = ?, can_edit = ?, can_delete = ? WHERE id = ?");
|
||||
$stmt->execute([$username, $full_name, $email, $role, $profile_image, $can_view, $can_add, $can_edit, $can_delete, $id]);
|
||||
}
|
||||
|
||||
// Update page permissions
|
||||
$pdo->prepare("DELETE FROM user_permissions WHERE user_id = ?")->execute([$id]);
|
||||
$perm_stmt = $pdo->prepare("INSERT INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
foreach ($modules as $module => $label) {
|
||||
$m_view = isset($_POST["perm_{$module}_view"]) ? 1 : 0;
|
||||
$m_add = isset($_POST["perm_{$module}_add"]) ? 1 : 0;
|
||||
$m_edit = isset($_POST["perm_{$module}_edit"]) ? 1 : 0;
|
||||
$m_delete = isset($_POST["perm_{$module}_delete"]) ? 1 : 0;
|
||||
$perm_stmt->execute([$id, $module, $m_view, $m_add, $m_edit, $m_delete]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
// Refresh own session if editing self
|
||||
if ($id == $_SESSION['user_id']) {
|
||||
unset($_SESSION['permissions']);
|
||||
}
|
||||
|
||||
$_SESSION['success'] = 'تم تحديث بيانات المستخدم بنجاح';
|
||||
redirect('users.php');
|
||||
} catch (PDOException $e) {
|
||||
if (isset($pdo)) $pdo->rollBack();
|
||||
$error = 'حدث خطأ: ' . $e->getMessage();
|
||||
}
|
||||
} else {
|
||||
$error = 'يرجى ملء جميع الحقول المطلوبة';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET['action']) && $_GET['action'] === 'delete' && isset($_GET['id'])) {
|
||||
if (!canDelete('users')) redirect('users.php');
|
||||
if ($_GET['id'] != $_SESSION['user_id']) {
|
||||
$stmt = db()->prepare("DELETE FROM users WHERE id = ?");
|
||||
$stmt->execute([$_GET['id']]);
|
||||
$_SESSION['success'] = 'تم حذف المستخدم بنجاح';
|
||||
redirect('users.php');
|
||||
} else {
|
||||
$error = 'لا يمكنك حذف حسابك الحالي';
|
||||
}
|
||||
}
|
||||
|
||||
// Get session messages
|
||||
if (isset($_SESSION['success'])) {
|
||||
$success = $_SESSION['success'];
|
||||
unset($_SESSION['success']);
|
||||
}
|
||||
if (isset($_SESSION['error'])) {
|
||||
$error = $_SESSION['error'];
|
||||
unset($_SESSION['error']);
|
||||
}
|
||||
|
||||
// Pagination
|
||||
$page = $_GET['page'] ?? 1;
|
||||
$perPage = 10;
|
||||
$totalUsers = db()->query("SELECT COUNT(*) FROM users")->fetchColumn();
|
||||
$pagination = getPagination($page, $totalUsers, $perPage);
|
||||
|
||||
$stmt = db()->prepare("SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?");
|
||||
$stmt->bindValue(1, $pagination['limit'], PDO::PARAM_INT);
|
||||
$stmt->bindValue(2, $pagination['offset'], PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$users = $stmt->fetchAll();
|
||||
|
||||
// Fetch permissions for all users
|
||||
$user_perms = [];
|
||||
$perm_stmt = db()->query("SELECT * FROM user_permissions");
|
||||
while ($row = $perm_stmt->fetch()) {
|
||||
$user_perms[$row['user_id']][$row['page']] = $row;
|
||||
}
|
||||
|
||||
// Handle Deep Link for Edit
|
||||
$deepLinkData = null;
|
||||
if (isset($_GET['action']) && $_GET['action'] === 'edit' && isset($_GET['id'])) {
|
||||
$stmt = db()->prepare("SELECT * FROM users WHERE id = ?");
|
||||
$stmt->execute([$_GET['id']]);
|
||||
$deepLinkData = $stmt->fetch();
|
||||
if ($deepLinkData) {
|
||||
$deepLinkData['page_permissions'] = $user_perms[$deepLinkData['id']] ?? [];
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<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('users')): ?>
|
||||
<button type="button" class="btn btn-primary shadow-sm" onclick="openUserModal('add')">
|
||||
<i class="fas fa-user-plus me-1"></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 mb-4">
|
||||
<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 class="pe-4 text-center">الإجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<tr>
|
||||
<td class="ps-4">
|
||||
<?php if ($user['profile_image']): ?>
|
||||
<img src="<?= $user['profile_image'] ?>" alt="Profile" class="rounded-circle shadow-sm" style="width: 40px; height: 40px; object-fit: cover;">
|
||||
<?php else: ?>
|
||||
<div class="rounded-circle bg-light d-inline-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
|
||||
<i class="fas fa-user text-secondary"></i>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="fw-bold"><?= htmlspecialchars($user['full_name']) ?></td>
|
||||
<td><?= htmlspecialchars($user['username']) ?></td>
|
||||
<td><?= htmlspecialchars($user['email'] ?? '-') ?></td>
|
||||
<td>
|
||||
<?php if ($user['role'] === 'admin'): ?>
|
||||
<span class="badge bg-danger">مدير</span>
|
||||
<?php elseif ($user['role'] === 'clerk'): ?>
|
||||
<span class="badge bg-warning text-dark">كاتب</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary">موظف</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= $user['created_at'] ?></td>
|
||||
<td class="pe-4 text-center">
|
||||
<?php if (canEdit('users')): ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="openUserModal('edit', <?= htmlspecialchars(json_encode(array_merge($user, ['page_permissions' => $user_perms[$user['id']] ?? []])), ENT_QUOTES, 'UTF-8') ?>)">
|
||||
<i class="fas fa-edit"></i> تعديل
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if (canDelete('users') && $user['id'] != $_SESSION['user_id']): ?>
|
||||
<a href="javascript:void(0)" onclick="confirmDelete(<?= $user['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 class="card-footer bg-white">
|
||||
<?= renderPagination($pagination['current_page'], $pagination['total_pages']) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Modal -->
|
||||
<div class="modal fade" id="userModal" tabindex="-1" aria-labelledby="userModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title fw-bold" id="userModalLabel">إضافة مستخدم جديد</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="userForm" method="POST" enctype="multipart/form-data">
|
||||
<div class="modal-body p-4">
|
||||
<input type="hidden" name="action" id="modalAction" value="add">
|
||||
<input type="hidden" name="id" id="modalId" value="0">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">الاسم الكامل</label>
|
||||
<input type="text" name="full_name" id="modalFullName" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">اسم المستخدم</label>
|
||||
<input type="text" name="username" id="modalUsername" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">البريد الإلكتروني</label>
|
||||
<input type="email" name="email" id="modalEmail" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">الصورة الشخصية</label>
|
||||
<input type="file" name="profile_image" id="modalProfileImage" class="form-control" accept="image/*">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">كلمة المرور <span id="pwdHint" class="text-muted small"></span></label>
|
||||
<input type="password" name="password" id="modalPassword" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-bold">الدور</label>
|
||||
<select name="role" id="modalRole" class="form-select" onchange="applyRolePresets(this.value)">
|
||||
<option value="staff">موظف</option>
|
||||
<option value="clerk">كاتب</option>
|
||||
<option value="admin">مدير</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h6 class="fw-bold mb-3"><i class="fas fa-lock me-2 text-primary"></i> صلاحيات الوصول لكل صفحة</h6>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm align-middle">
|
||||
<thead class="bg-light">
|
||||
<tr class="text-center">
|
||||
<th class="text-start ps-3">الصفحة / الموديول</th>
|
||||
<th>عرض</th>
|
||||
<th>إضافة</th>
|
||||
<th>تعديل</th>
|
||||
<th>حذف</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($modules as $key => $label): ?>
|
||||
<tr class="text-center">
|
||||
<td class="text-start ps-3 fw-bold"><?= $label ?></td>
|
||||
<td>
|
||||
<input class="form-check-input" type="checkbox" name="perm_<?= $key ?>_view" id="perm_<?= $key ?>_view" value="1">
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-check-input" type="checkbox" name="perm_<?= $key ?>_add" id="perm_<?= $key ?>_add" value="1">
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-check-input" type="checkbox" name="perm_<?= $key ?>_edit" id="perm_<?= $key ?>_edit" value="1">
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-check-input" type="checkbox" name="perm_<?= $key ?>_delete" id="perm_<?= $key ?>_delete" value="1">
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Hidden legacy fields for backward compatibility if needed -->
|
||||
<input type="hidden" name="can_view_global" id="permViewGlobal" value="1">
|
||||
<input type="hidden" name="can_add_global" id="permAddGlobal" value="0">
|
||||
<input type="hidden" name="can_edit_global" id="permEditGlobal" value="0">
|
||||
<input type="hidden" name="can_delete_global" id="permDeleteGlobal" value="0">
|
||||
</div>
|
||||
<div class="modal-footer bg-light">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary px-4">حفظ البيانات</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let userModal;
|
||||
const modules = <?= json_encode(array_keys($modules)) ?>;
|
||||
|
||||
function applyRolePresets(role) {
|
||||
modules.forEach(m => {
|
||||
const view = document.getElementById(`perm_${m}_view`);
|
||||
const add = document.getElementById(`perm_${m}_add`);
|
||||
const edit = document.getElementById(`perm_${m}_edit`);
|
||||
const del = document.getElementById(`perm_${m}_delete`);
|
||||
|
||||
if (role === 'admin') {
|
||||
view.checked = add.checked = edit.checked = del.checked = true;
|
||||
} else if (role === 'clerk') {
|
||||
if (m === 'users' || m === 'settings') {
|
||||
view.checked = add.checked = edit.checked = del.checked = false;
|
||||
} else {
|
||||
view.checked = add.checked = edit.checked = true;
|
||||
del.checked = false; // Clerk can't delete by default
|
||||
}
|
||||
} else {
|
||||
if (m === 'inbound' || m === 'outbound' || m === 'internal') {
|
||||
view.checked = true;
|
||||
add.checked = (m === 'internal'); // Staff can send internal mail
|
||||
edit.checked = del.checked = false;
|
||||
} else {
|
||||
view.checked = add.checked = edit.checked = del.checked = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openUserModal(action, data = null) {
|
||||
if (!userModal) {
|
||||
const modalEl = document.getElementById('userModal');
|
||||
if (typeof bootstrap !== 'undefined') {
|
||||
userModal = new bootstrap.Modal(modalEl);
|
||||
} else {
|
||||
console.error('Bootstrap not loaded');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const label = document.getElementById('userModalLabel');
|
||||
const modalAction = document.getElementById('modalAction');
|
||||
const modalId = document.getElementById('modalId');
|
||||
const modalPassword = document.getElementById('modalPassword');
|
||||
const pwdHint = document.getElementById('pwdHint');
|
||||
|
||||
const fields = {
|
||||
full_name: document.getElementById('modalFullName'),
|
||||
username: document.getElementById('modalUsername'),
|
||||
email: document.getElementById('modalEmail'),
|
||||
role: document.getElementById('modalRole')
|
||||
};
|
||||
|
||||
modalAction.value = action;
|
||||
document.getElementById('modalProfileImage').value = '';
|
||||
|
||||
if (action === 'add') {
|
||||
label.textContent = 'إضافة مستخدم جديد';
|
||||
modalId.value = '0';
|
||||
Object.keys(fields).forEach(key => fields[key].value = '');
|
||||
modalRole.value = 'staff';
|
||||
applyRolePresets('staff');
|
||||
modalPassword.required = true;
|
||||
pwdHint.textContent = '';
|
||||
} else {
|
||||
label.textContent = 'تعديل بيانات المستخدم';
|
||||
modalId.value = data.id;
|
||||
Object.keys(fields).forEach(key => {
|
||||
if (fields[key]) fields[key].value = data[key] || '';
|
||||
});
|
||||
|
||||
// Set permissions checkboxes
|
||||
modules.forEach(m => {
|
||||
const p = data.page_permissions && data.page_permissions[m] ? data.page_permissions[m] : {};
|
||||
document.getElementById(`perm_${m}_view`).checked = p.can_view == 1;
|
||||
document.getElementById(`perm_${m}_add`).checked = p.can_add == 1;
|
||||
document.getElementById(`perm_${m}_edit`).checked = p.can_edit == 1;
|
||||
document.getElementById(`perm_${m}_delete`).checked = p.can_delete == 1;
|
||||
});
|
||||
|
||||
modalPassword.required = false;
|
||||
pwdHint.textContent = '(اتركه فارغاً للحفاظ على كلمة المرور الحالية)';
|
||||
}
|
||||
|
||||
userModal.show();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
<?php if ($deepLinkData): ?>
|
||||
openUserModal('edit', <?= json_encode($deepLinkData) ?>);
|
||||
<?php elseif (isset($_GET['action']) && $_GET['action'] === 'add'): ?>
|
||||
openUserModal('add');
|
||||
<?php endif; ?>
|
||||
});
|
||||
|
||||
function confirmDelete(id) {
|
||||
if (typeof Swal === 'undefined') {
|
||||
if (confirm('هل أنت متأكد من الحذف؟')) {
|
||||
window.location.href = 'users.php?action=delete&id=' + id;
|
||||
}
|
||||
return;
|
||||
}
|
||||
Swal.fire({
|
||||
title: 'هل أنت متأكد؟',
|
||||
text: "لا يمكن التراجع عن عملية الحذف!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
cancelButtonColor: '#3085d6',
|
||||
confirmButtonText: 'نعم، احذف!',
|
||||
cancelButtonText: 'إلغاء'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = 'users.php?action=delete&id=' + id;
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.modal-content {
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.modal-header.bg-primary {
|
||||
background-color: #0d6efd !important;
|
||||
}
|
||||
.form-check-input:checked {
|
||||
background-color: #198754;
|
||||
border-color: #198754;
|
||||
}
|
||||
.table-sm th, .table-sm td {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php';
|
||||
515
view_mail.php
Normal file
515
view_mail.php
Normal file
@ -0,0 +1,515 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
require_once __DIR__ . '/m_services/MailService.php';
|
||||
|
||||
$id = $_GET['id'] ?? 0;
|
||||
$type = $_GET['type'] ?? '';
|
||||
|
||||
if (!$id) redirect('index.php');
|
||||
|
||||
// If type is not provided, try to find it in any of the tables (for backward compatibility if any links were missed)
|
||||
if (!$type) {
|
||||
foreach (['inbound', 'outbound', 'internal'] as $t) {
|
||||
$table = $t . '_mail';
|
||||
$check = db()->prepare("SELECT id FROM $table WHERE id = ?");
|
||||
$check->execute([$id]);
|
||||
if ($check->fetch()) {
|
||||
$type = $t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$type) redirect('index.php');
|
||||
|
||||
$table_mail = $type . '_mail';
|
||||
$table_attachments = $type . '_attachments';
|
||||
$table_comments = $type . '_comments';
|
||||
|
||||
$stmt = db()->prepare("SELECT m.*, u1.full_name as assigned_name, u2.full_name as creator_name,
|
||||
s.name as status_name, s.color as status_color
|
||||
FROM $table_mail m
|
||||
LEFT JOIN users u1 ON m.assigned_to = u1.id
|
||||
LEFT JOIN users u2 ON m.created_by = u2.id
|
||||
LEFT JOIN mailbox_statuses s ON m.status_id = s.id
|
||||
WHERE m.id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$mail = $stmt->fetch();
|
||||
|
||||
if (!$mail) redirect('index.php');
|
||||
|
||||
// Add back the type for logic below
|
||||
$mail['type'] = $type;
|
||||
|
||||
// Check if user has view permission for this mail type
|
||||
if (!canView($type)) {
|
||||
redirect('index.php');
|
||||
}
|
||||
|
||||
// Security check for internal mail: only sender or recipient can view
|
||||
if ($type === 'internal') {
|
||||
if ($mail['created_by'] != $_SESSION['user_id'] && $mail['assigned_to'] != $_SESSION['user_id']) {
|
||||
redirect('internal_inbox.php');
|
||||
}
|
||||
}
|
||||
|
||||
$success = '';
|
||||
$error = '';
|
||||
|
||||
// Handle Comment submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_comment'])) {
|
||||
if ($type !== 'internal' && !canEdit($type)) {
|
||||
$error = 'عذراً، ليس لديك الصلاحية لإضافة تعليقات';
|
||||
} else {
|
||||
$comment = $_POST['comment'] ?? '';
|
||||
$referred_user_id = $_POST['referred_user_id'] ?: null;
|
||||
|
||||
if ($comment) {
|
||||
$stmt = db()->prepare("INSERT INTO $table_comments (mail_id, user_id, comment, referred_user_id) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$id, $_SESSION['user_id'], $comment, $referred_user_id]);
|
||||
|
||||
// Send email notification if referred
|
||||
if ($referred_user_id) {
|
||||
$stmt_u = db()->prepare("SELECT email, full_name FROM users WHERE id = ?");
|
||||
$stmt_u->execute([$referred_user_id]);
|
||||
$referred_user = $stmt_u->fetch();
|
||||
|
||||
if ($referred_user && !empty($referred_user['email'])) {
|
||||
$sender_name = $_SESSION['name'] ?? 'زميلك';
|
||||
$mail_subject = "إحالة بريد: " . $mail['subject'];
|
||||
$mail_link = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]" . dirname($_SERVER['PHP_SELF']) . "/view_mail.php?id=" . $id . "&type=" . $type;
|
||||
|
||||
$html = "
|
||||
<div dir='rtl'>
|
||||
<h3>مرحباً " . htmlspecialchars($referred_user['full_name']) . "</h3>
|
||||
<p>قام <strong>" . htmlspecialchars($sender_name) . "</strong> بإحالة بريد إليك مع التعليق التالي:</p>
|
||||
<blockquote style='background: #f9f9f9; padding: 10px; border-left: 5px solid #ccc;'>
|
||||
" . nl2br(htmlspecialchars($comment)) . "
|
||||
</blockquote>
|
||||
<p><strong>تفاصيل البريد:</strong></p>
|
||||
<ul>
|
||||
<li><strong>رقم القيد:</strong> " . htmlspecialchars($mail['ref_no']) . "</li>
|
||||
<li><strong>الموضوع:</strong> " . htmlspecialchars($mail['subject']) . "</li>
|
||||
</ul>
|
||||
<p><a href='{$mail_link}' style='display: inline-block; padding: 10px 20px; background-color: #007bff; color: white; text-decoration: none; border-radius: 5px;'>عرض البريد</a></p>
|
||||
</div>
|
||||
";
|
||||
|
||||
$txt = "قام {$sender_name} بإحالة بريد إليك: {$mail['subject']}\n\nالتعليق: {$comment}\n\nعرض البريد: {$mail_link}";
|
||||
|
||||
MailService::sendMail($referred_user['email'], $mail_subject, $html, $txt);
|
||||
}
|
||||
}
|
||||
|
||||
$_SESSION['success'] = 'تم إضافة التعليق بنجاح';
|
||||
redirect("view_mail.php?id=$id&type=$type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Attachment upload
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['attachment'])) {
|
||||
if ($type !== 'internal' && !canEdit($type)) {
|
||||
$error = 'عذراً، ليس لديك الصلاحية لرفع مرفقات';
|
||||
} else {
|
||||
$file = $_FILES['attachment'];
|
||||
$display_name = $_POST['display_name'] ?? '';
|
||||
if ($file['error'] === 0) {
|
||||
$upload_dir = 'uploads/attachments/';
|
||||
if (!is_dir($upload_dir)) mkdir($upload_dir, 0777, true);
|
||||
|
||||
$file_name = time() . '_' . basename($file['name']);
|
||||
$target_path = $upload_dir . $file_name;
|
||||
|
||||
if (move_uploaded_file($file['tmp_name'], $target_path)) {
|
||||
$stmt = db()->prepare("INSERT INTO $table_attachments (mail_id, display_name, file_path, file_name, file_size) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$id, $display_name, $target_path, $file['name'], $file['size']]);
|
||||
$_SESSION['success'] = 'تم رفع الملف بنجاح';
|
||||
redirect("view_mail.php?id=$id&type=$type");
|
||||
} else {
|
||||
$error = 'فشل في رفع الملف';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Attachment deletion
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_attachment'])) {
|
||||
if ($type !== 'internal' && !canDelete($type)) {
|
||||
$error = 'عذراً، ليس لديك الصلاحية لحذف المرفقات';
|
||||
} else {
|
||||
$attachment_id = $_POST['attachment_id'] ?? 0;
|
||||
if ($attachment_id) {
|
||||
$stmt = db()->prepare("SELECT * FROM $table_attachments WHERE id = ?");
|
||||
$stmt->execute([$attachment_id]);
|
||||
$attachment = $stmt->fetch();
|
||||
|
||||
if ($attachment) {
|
||||
if (file_exists($attachment['file_path'])) {
|
||||
unlink($attachment['file_path']);
|
||||
}
|
||||
$stmt = db()->prepare("DELETE FROM $table_attachments WHERE id = ?");
|
||||
$stmt->execute([$attachment_id]);
|
||||
$_SESSION['success'] = 'تم حذف المرفق بنجاح';
|
||||
redirect("view_mail.php?id=$id&type=$type");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get session messages
|
||||
if (isset($_SESSION['success'])) {
|
||||
$success = $_SESSION['success'];
|
||||
unset($_SESSION['success']);
|
||||
}
|
||||
if (isset($_SESSION['error'])) {
|
||||
$error = $_SESSION['error'];
|
||||
unset($_SESSION['error']);
|
||||
}
|
||||
|
||||
$comments_stmt = db()->prepare("SELECT c.*, u.full_name, ru.full_name as referred_name
|
||||
FROM $table_comments c
|
||||
LEFT JOIN users u ON c.user_id = u.id
|
||||
LEFT JOIN users ru ON c.referred_user_id = ru.id
|
||||
WHERE c.mail_id = ? ORDER BY c.created_at DESC");
|
||||
$comments_stmt->execute([$id]);
|
||||
$mail_comments = $comments_stmt->fetchAll();
|
||||
|
||||
$attachments_stmt = db()->prepare("SELECT * FROM $table_attachments WHERE mail_id = ? ORDER BY created_at DESC");
|
||||
$attachments_stmt->execute([$id]);
|
||||
$mail_attachments = $attachments_stmt->fetchAll();
|
||||
|
||||
// Fetch all users for referral dropdown
|
||||
$stmt_users = db()->prepare("SELECT id, full_name, role FROM users WHERE id != ? ORDER BY full_name ASC");
|
||||
$stmt_users->execute([$_SESSION['user_id']]);
|
||||
$all_users = $stmt_users->fetchAll();
|
||||
|
||||
function isPreviewable($fileName) {
|
||||
$ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
|
||||
return in_array($ext, ['pdf', 'png', 'jpg', 'jpeg', 'gif', 'webp']);
|
||||
}
|
||||
|
||||
$type_label = 'بريد وارد';
|
||||
if ($type == 'outbound') $type_label = 'بريد صادر';
|
||||
if ($type == 'internal') $type_label = 'رسالة داخلية';
|
||||
|
||||
$back_link = $type . '.php';
|
||||
if ($type == 'internal') {
|
||||
$back_link = ($mail['created_by'] == $_SESSION['user_id']) ? 'internal_outbox.php' : 'internal_inbox.php';
|
||||
}
|
||||
?>
|
||||
|
||||
<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">تفاصيل <?= $type_label ?></h1>
|
||||
<div class="btn-group">
|
||||
<a href="<?= $back_link ?>" class="btn btn-outline-secondary">عودة للقائمة</a>
|
||||
<?php if ($type !== 'internal' && canEdit($type)): ?>
|
||||
<a href="<?= $type ?>.php?action=edit&id=<?= $mail['id'] ?>" class="btn btn-outline-primary">تعديل البيانات</a>
|
||||
<?php if ($type === 'outbound'): ?>
|
||||
<a href="print_outbound.php?id=<?= $mail['id'] ?>" target="_blank" class="btn btn-outline-secondary"><i class="fas fa-print me-1"></i> طباعة</a>
|
||||
<?php elseif ($type === 'inbound'): ?>
|
||||
<a href="print_inbound.php?id=<?= $mail['id'] ?>" target="_blank" class="btn btn-outline-secondary"><i class="fas fa-print me-1"></i> طباعة</a>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</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="row">
|
||||
<!-- Mail Details -->
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0 fw-bold">المعلومات الأساسية</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="text-muted small">رقم القيد</label>
|
||||
<p class="fw-bold fs-5 text-primary"><?= $mail['ref_no'] ?></p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="text-muted small">تاريخ التسجيل</label>
|
||||
<p class="fw-bold"><?= $mail['date_registered'] ?></p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="text-muted small">الموعد النهائي</label>
|
||||
<p class="fw-bold">
|
||||
<?php if ($mail['due_date']): ?>
|
||||
<span class="<?= (strtotime($mail['due_date']) < time() && $mail['status_name'] != 'closed') ? 'text-danger' : '' ?>">
|
||||
<?= $mail['due_date'] ?>
|
||||
<?php if (strtotime($mail['due_date']) < time() && $mail['status_name'] != 'closed'): ?>
|
||||
<i class="fas fa-exclamation-triangle ms-1"></i>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">غير محدد</span>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="text-muted small">الحالة</label>
|
||||
<p>
|
||||
<?php
|
||||
$s_name = $mail['status_name'] ?? 'received';
|
||||
$s_color = $mail['status_color'] ?? '#6c757d';
|
||||
$d_name = $s_name;
|
||||
if ($s_name == 'received') $d_name = ($type == 'internal' ? 'جديد / مرسل' : 'تم الاستلام');
|
||||
if ($s_name == 'in_progress') $d_name = 'قيد المعالجة';
|
||||
if ($s_name == 'closed') $d_name = ($type == 'internal' ? 'مؤرشف' : 'مكتمل');
|
||||
?>
|
||||
<span class="badge" style="background-color: <?= $s_color ?>;"><?= htmlspecialchars($d_name) ?></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="text-muted small">الموضوع</label>
|
||||
<div class="fw-bold">
|
||||
<?php
|
||||
if ($type == 'outbound' || $type == 'internal') {
|
||||
echo $mail['subject'];
|
||||
} else {
|
||||
echo htmlspecialchars($mail['subject']);
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($type == 'internal'): ?>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small">المرسل</label>
|
||||
<p class="fw-bold text-primary"><?= htmlspecialchars($mail['creator_name']) ?></p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small">المستلم</label>
|
||||
<p class="fw-bold text-success"><?= htmlspecialchars($mail['assigned_name']) ?></p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small"><?= $type == 'inbound' ? 'المرسل' : 'المرسل الداخلي' ?></label>
|
||||
<p><?= htmlspecialchars($mail['sender'] ?: 'غير محدد') ?></p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small"><?= $type == 'inbound' ? 'المستلم الداخلي' : 'الجهة المستلمة' ?></label>
|
||||
<p><?= htmlspecialchars($mail['recipient'] ?: 'غير محدد') ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="text-muted small">الرسالة / الوصف</label>
|
||||
<div class="bg-light p-3 rounded border">
|
||||
<?php
|
||||
if ($type == 'outbound' || $type == 'internal') {
|
||||
echo $mail['description'] ?: '<span class="text-muted">لا يوجد محتوى إضافي</span>';
|
||||
} else {
|
||||
echo nl2br(htmlspecialchars($mail['description'] ?: 'لا يوجد محتوى إضافي'));
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($type != 'internal'): ?>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small">الموظف المسؤول</label>
|
||||
<p><?= htmlspecialchars($mail['assigned_name'] ?: 'غير معين') ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="col-md-6 <?= $type == 'internal' ? 'col-md-12' : '' ?> text-end">
|
||||
<label class="text-muted small">تاريخ الإنشاء</label>
|
||||
<p class="text-muted small"><?= $mail['created_at'] ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comments -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0 fw-bold">الردود والمتابعة</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if ($type === 'internal' || canEdit($type)): ?>
|
||||
<form method="POST" class="mb-4 bg-light p-3 rounded border">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-bold">إضافة <?= $type == 'internal' ? 'رد' : 'تعليق' ?></label>
|
||||
<textarea name="comment" class="form-control" rows="2" placeholder="اكتب ردك هنا..." required></textarea>
|
||||
</div>
|
||||
<?php if ($type != 'internal'): ?>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold">إحالة إلى موظف (اختياري)</label>
|
||||
<select name="referred_user_id" class="form-select form-select-sm">
|
||||
<option value="">-- اختر موظفاً للإحالة --</option>
|
||||
<?php foreach ($all_users as $u): ?>
|
||||
<option value="<?= $u['id'] ?>"><?= htmlspecialchars($u['full_name']) ?> (<?= ucfirst($u['role']) ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<button type="submit" name="add_comment" class="btn btn-sm btn-primary">إرسال <?= $type == 'internal' ? 'الرد' : 'التعليق' ?></button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="comment-list">
|
||||
<?php if ($mail_comments): foreach ($mail_comments as $c): ?>
|
||||
<div class="border-bottom pb-2 mb-2">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<span class="fw-bold text-primary"><?= htmlspecialchars($c['full_name']) ?></span>
|
||||
<?php if ($c['referred_name']): ?>
|
||||
<span class="badge bg-info text-dark ms-2 small">
|
||||
<i class="fas fa-share ms-1"></i> إحالة إلى: <?= htmlspecialchars($c['referred_name']) ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<span class="text-muted small"><?= $c['created_at'] ?></span>
|
||||
</div>
|
||||
<p class="mb-0 small"><?= nl2br(htmlspecialchars($c['comment'])) ?></p>
|
||||
</div>
|
||||
<?php endforeach; else: ?>
|
||||
<p class="text-center text-muted small">لا توجد ردود بعد</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attachments Side -->
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0 fw-bold">المرفقات</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if ($type === 'internal' || canEdit($type)): ?>
|
||||
<form method="POST" enctype="multipart/form-data" class="mb-4">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small mb-1">اسم المرفق (يظهر في القائمة)</label>
|
||||
<input type="text" name="display_name" class="form-control form-control-sm mb-2" placeholder="مثال: صورة، مستند...">
|
||||
<label class="form-label small mb-1">اختر الملف</label>
|
||||
<input type="file" name="attachment" class="form-control form-control-sm" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-sm btn-secondary w-100">رفع ملف</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="list-group list-group-flush">
|
||||
<?php if ($mail_attachments): foreach ($mail_attachments as $a): ?>
|
||||
<div class="list-group-item px-0">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center">
|
||||
<div class="text-truncate" style="max-width: 150px;">
|
||||
<i class="fas fa-file me-2 text-muted"></i>
|
||||
<a href="<?= $a['file_path'] ?>" target="_blank" class="text-decoration-none small">
|
||||
<?= htmlspecialchars($a['display_name'] ?: $a['file_name']) ?>
|
||||
</a>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="text-muted small me-2"><?= round($a['file_size'] / 1024, 1) ?> KB</span>
|
||||
<?php if (isPreviewable($a['file_name'])): ?>
|
||||
<button class="btn btn-link btn-sm p-0 text-primary preview-btn me-2"
|
||||
data-file="<?= $a['file_path'] ?>"
|
||||
data-name="<?= htmlspecialchars($a['display_name'] ?: $a['file_name']) ?>"
|
||||
title="معاينة">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($type == 'internal' || canDelete($type)): ?>
|
||||
<form method="POST" class="d-inline delete-attachment-form">
|
||||
<input type="hidden" name="attachment_id" value="<?= $a['id'] ?>">
|
||||
<input type="hidden" name="delete_attachment" value="1">
|
||||
<button type="button" class="btn btn-link btn-sm p-0 text-danger delete-btn" title="حذف">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; else: ?>
|
||||
<p class="text-center text-muted small py-3">لا توجد مرفقات</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Modal -->
|
||||
<div class="modal fade" id="previewModal" tabindex="-1" aria-labelledby="previewModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="previewModalLabel">معاينة الملف</h5>
|
||||
<button type="button" class="btn-close ms-0 me-auto" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0 bg-dark d-flex align-items-center justify-content-center" style="min-height: 80vh;">
|
||||
<div id="previewContainer" class="w-100 h-100 text-center"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a id="downloadBtn" href="#" class="btn btn-primary" download>تحميل الملف</a>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إغلاق</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const previewModalEl = document.getElementById('previewModal');
|
||||
let previewModal;
|
||||
if (typeof bootstrap !== 'undefined') {
|
||||
previewModal = new bootstrap.Modal(previewModalEl);
|
||||
}
|
||||
const previewContainer = document.getElementById('previewContainer');
|
||||
const previewModalLabel = document.getElementById('previewModalLabel');
|
||||
const downloadBtn = document.getElementById('downloadBtn');
|
||||
|
||||
document.querySelectorAll('.preview-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const filePath = this.getAttribute('data-file');
|
||||
const fileName = this.getAttribute('data-name');
|
||||
const ext = filePath.split('.').pop().toLowerCase();
|
||||
|
||||
previewModalLabel.textContent = 'معاينة: ' + fileName;
|
||||
downloadBtn.href = filePath;
|
||||
previewContainer.innerHTML = '';
|
||||
|
||||
if (ext === 'pdf') {
|
||||
previewContainer.innerHTML = `<iframe src="${filePath}" width="100%" height="800px" style="border: none;"></iframe>`;
|
||||
} else if (['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(ext)) {
|
||||
previewContainer.innerHTML = `<img src="${filePath}" class="img-fluid" style="max-height: 80vh;">`;
|
||||
} else {
|
||||
previewContainer.innerHTML = '<div class="text-white p-5">هذا النوع من الملفات غير مدعوم للمعاينة المباشرة</div>';
|
||||
}
|
||||
|
||||
if (previewModal) previewModal.show();
|
||||
});
|
||||
});
|
||||
|
||||
previewModalEl.addEventListener('hidden.bs.modal', function () {
|
||||
previewContainer.innerHTML = '';
|
||||
});
|
||||
|
||||
document.querySelectorAll('.delete-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const form = this.closest('form');
|
||||
if (confirm('هل أنت متأكد من الحذف؟')) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
Loading…
x
Reference in New Issue
Block a user