Compare commits

..

55 Commits

Author SHA1 Message Date
Flatlogic Bot
79a5056e8e Edit expense_reports.php via Editor 2026-03-27 18:07:30 +00:00
Flatlogic Bot
ebc8803ddd Edit meetings.php via Editor 2026-03-27 18:04:57 +00:00
Flatlogic Bot
a5a5101374 Edit print_meeting.php via Editor 2026-03-27 18:03:20 +00:00
Flatlogic Bot
cf34144fdb Edit balance_sheet.php via Editor 2026-03-27 18:00:55 +00:00
Flatlogic Bot
7828e2ad4e add paginations to all modules 2026-03-27 09:55:11 +00:00
Flatlogic Bot
1b3577b917 add paginations 2026-03-27 09:36:07 +00:00
Flatlogic Bot
4ee179ebfe update meeting 2026-03-27 09:29:56 +00:00
Flatlogic Bot
07691b3ce6 updaing meeting 2026-03-27 07:10:38 +00:00
Flatlogic Bot
10d64f8648 Autosave: 20260327-063637 2026-03-27 06:36:37 +00:00
Flatlogic Bot
518c9fdd19 update migration 2026-03-27 05:43:49 +00:00
Flatlogic Bot
e11d717222 updating migration file 2026-03-27 04:07:31 +00:00
Flatlogic Bot
55701e7000 Edit login.php via Editor 2026-03-27 03:57:16 +00:00
Flatlogic Bot
110626a686 Autosave: 20260327-034455 2026-03-27 03:44:56 +00:00
Flatlogic Bot
d38b70650f stock module 2026-03-27 03:32:55 +00:00
Flatlogic Bot
24ab40453b adding migrate.php 2026-03-12 07:23:17 +00:00
Flatlogic Bot
91600dc6f7 adding app title in login 2026-03-12 07:18:36 +00:00
Flatlogic Bot
17c4e0e1ef editing login width 2026-03-12 07:14:19 +00:00
Flatlogic Bot
d82e9249b7 Autosave: 20260312-071130 2026-03-12 07:11:31 +00:00
Flatlogic Bot
cc73fe5d8d Autosave: 20260312-031128 2026-03-12 03:11:28 +00:00
Flatlogic Bot
b573f5ceaa Autosave: 20260312-023732 2026-03-12 02:37:32 +00:00
Flatlogic Bot
2a4914959c Autosave: 20260312-021439 2026-03-12 02:14:39 +00:00
Flatlogic Bot
813c9596f7 Autosave: 20260311-183230 2026-03-11 18:32:30 +00:00
Flatlogic Bot
6ede6271b7 Autosave: 20260311-175406 2026-03-11 17:54:07 +00:00
Flatlogic Bot
36d1266f5e add slogan 2026-03-01 05:44:43 +00:00
Flatlogic Bot
9e429aea35 inbound update 2026-03-01 05:26:38 +00:00
Flatlogic Bot
85c1b9dd67 updating install file 2026-03-01 05:16:39 +00:00
Flatlogic Bot
ad6d09dcf3 add migration 14 2026-03-01 03:37:35 +00:00
Flatlogic Bot
efe588ff68 editing printing 2026-03-01 03:17:52 +00:00
Flatlogic Bot
e9893232d6 Autosave: 20260301-025324 2026-03-01 02:53:25 +00:00
Flatlogic Bot
627842bf5c adding editor 2026-02-28 18:32:41 +00:00
Flatlogic Bot
6ddb4f9f37 Autosave: 20260228-102254 2026-02-28 10:22:54 +00:00
Flatlogic Bot
dcb1aa0c6b update 2221 2026-02-28 09:40:33 +00:00
Flatlogic Bot
c5ac8e3c6e splitting db 2026-02-28 08:22:46 +00:00
Flatlogic Bot
c6cb25129c installation 2026-02-28 07:59:16 +00:00
Flatlogic Bot
26c455a51e update 10 2026-02-28 07:39:56 +00:00
Flatlogic Bot
e93f08d5e0 update installation 2026-02-28 07:24:57 +00:00
Flatlogic Bot
fcd101fb65 install update 2026-02-28 07:16:11 +00:00
Flatlogic Bot
b304da2bea change install file 2026-02-28 07:03:36 +00:00
Flatlogic Bot
10eedb49b2 install file 2 2026-02-28 06:01:25 +00:00
Flatlogic Bot
d2bc35de8c fix install file 2026-02-28 05:55:01 +00:00
Flatlogic Bot
cfc04e4af0 addin install.php 2026-02-28 05:47:52 +00:00
Flatlogic Bot
6f244ec88a editing user profile 2026-02-28 05:37:18 +00:00
Flatlogic Bot
37abbe5d1e adding permissions 2026-02-28 05:28:11 +00:00
Flatlogic Bot
607b9d8838 updating internal mail 2026-02-28 03:56:18 +00:00
Flatlogic Bot
3aa10f537e Autosave: 20260228-033101 2026-02-28 03:31:01 +00:00
Flatlogic Bot
e1febf7e84 editing forms redirections 2026-02-28 03:18:09 +00:00
Flatlogic Bot
fe46d13ff0 modifying login 2026-02-28 02:48:15 +00:00
Flatlogic Bot
90db1dc8a5 Autosave: 20260228-024736 2026-02-28 02:47:36 +00:00
Flatlogic Bot
21d9204f3a paginations 2026-02-28 02:29:08 +00:00
Flatlogic Bot
118aae16b0 adding permission 2026-02-27 18:38:01 +00:00
Flatlogic Bot
e70adf8720 Edit login.php via Editor 2026-02-27 18:24:16 +00:00
Flatlogic Bot
2900795488 Autosave: 20260227-182025 2026-02-27 18:20:25 +00:00
Flatlogic Bot
6d5518a7b7 Autosave: 20260227-174549 2026-02-27 17:45:49 +00:00
Flatlogic Bot
fc07d00bbb add profile 2026-02-27 17:32:01 +00:00
Flatlogic Bot
5d112ecb1b Autosave: 20260227-151712 2026-02-27 15:17:13 +00:00
99 changed files with 14550 additions and 578 deletions

372
accounting.php Normal file
View 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
View 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
View 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
View 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'; ?>

View File

@ -7,7 +7,7 @@ $input = json_decode(file_get_contents('php://input'), true);
$message = $input['message'] ?? ''; $message = $input['message'] ?? '';
if (empty($message)) { if (empty($message)) {
echo json_encode(['reply' => "I didn't catch that. Could you repeat?"]); echo json_encode(['reply' => "لم أفهم ذلك. هل يمكنك التكرار؟"]);
exit; exit;
} }
@ -16,18 +16,18 @@ try {
$stmt = db()->query("SELECT keywords, answer FROM faqs"); $stmt = db()->query("SELECT keywords, answer FROM faqs");
$faqs = $stmt->fetchAll(PDO::FETCH_ASSOC); $faqs = $stmt->fetchAll(PDO::FETCH_ASSOC);
$knowledgeBase = "Here is the knowledge base for this website:\n\n"; $knowledgeBase = "إليك قاعدة المعرفة لهذا الموقع:\n\n";
foreach ($faqs as $faq) { 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 // 2. Construct Prompt for AI
$systemPrompt = "You are a helpful, friendly AI assistant for this website. " . $systemPrompt = "أنت مساعد ذكاء اصطناعي مفيد وودود لهذا الموقع. " .
"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" . "اجعل الإجابات موجزة واحترافية وباللغة العربية.\n\n" .
$knowledgeBase; $knowledgeBase;
// 3. Call AI API // 3. Call AI API
@ -40,7 +40,12 @@ try {
]); ]);
if (!empty($response['success'])) { 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 // 4. Save to Database
try { try {
@ -55,10 +60,10 @@ try {
} else { } else {
// Fallback if AI fails // Fallback if AI fails
error_log("AI Error: " . ($response['error'] ?? 'Unknown')); 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) { } catch (Exception $e) {
error_log("Chat Error: " . $e->getMessage()); 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
View 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
View 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()]);
}

View File

@ -1,14 +1,204 @@
body { body {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); background-color: var(--bs-body-bg);
background-size: 400% 400%; color: var(--bs-body-color);
animation: gradient 15s ease infinite; font-family: 'Cairo', 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: #212529;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
min-height: 100vh; 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 { .main-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
@ -36,27 +226,26 @@ body {
.chat-container { .chat-container {
width: 100%; width: 100%;
max-width: 600px; max-width: 600px;
background: rgba(255, 255, 255, 0.85); background: var(--bs-body-bg);
border: 1px solid rgba(255, 255, 255, 0.3); border: 1px solid var(--bs-border-color);
border-radius: 20px; border-radius: 20px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 85vh; height: 85vh;
box-shadow: 0 20px 40px rgba(0,0,0,0.2); box-shadow: 0 10px 30px rgba(0,0,0,0.1);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
overflow: hidden; overflow: hidden;
} }
.chat-header { .chat-header {
padding: 1.5rem; padding: 1.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05); border-bottom: 1px solid var(--bs-border-color);
background: rgba(255, 255, 255, 0.5); background: var(--bs-tertiary-bg);
font-weight: 700; font-weight: 700;
font-size: 1.1rem; font-size: 1.1rem;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
color: var(--bs-body-color);
} }
.chat-messages { .chat-messages {
@ -78,12 +267,21 @@ body {
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3); background: rgba(0, 0, 0, 0.1);
border-radius: 10px; 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 { ::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5); background: rgba(0, 0, 0, 0.2);
} }
.message { .message {
@ -92,33 +290,34 @@ body {
border-radius: 16px; border-radius: 16px;
line-height: 1.5; line-height: 1.5;
font-size: 0.95rem; 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); animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
} }
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(20px) scale(0.95); } from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0) scale(1); } to { opacity: 1; transform: translateY(0); }
} }
.message.visitor { .message.visitor {
align-self: flex-end; align-self: flex-end;
background: linear-gradient(135deg, #212529 0%, #343a40 100%); background: #0d6efd;
color: #fff; color: #fff;
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
} }
.message.bot { .message.bot {
align-self: flex-start; align-self: flex-start;
background: #ffffff; background: var(--bs-tertiary-bg); /* Use theme bg */
color: #212529; color: var(--bs-body-color);
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
border: 1px solid var(--bs-border-color);
} }
.chat-input-area { .chat-input-area {
padding: 1.25rem; padding: 1.25rem;
background: rgba(255, 255, 255, 0.5); background: var(--bs-body-bg);
border-top: 1px solid rgba(0, 0, 0, 0.05); border-top: 1px solid var(--bs-border-color);
} }
.chat-input-area form { .chat-input-area form {
@ -128,21 +327,22 @@ body {
.chat-input-area input { .chat-input-area input {
flex: 1; flex: 1;
border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid var(--bs-border-color);
border-radius: 12px; border-radius: 12px;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
outline: none; outline: none;
background: rgba(255, 255, 255, 0.9); background: var(--bs-body-bg);
color: var(--bs-body-color);
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.chat-input-area input:focus { .chat-input-area input:focus {
border-color: #23a6d5; border-color: #0d6efd;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.1);
} }
.chat-input-area button { .chat-input-area button {
background: #212529; background: #0d6efd;
color: #fff; color: #fff;
border: none; border: none;
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
@ -153,98 +353,43 @@ body {
} }
.chat-input-area button:hover { .chat-input-area button:hover {
background: #000; background: #0b5ed7;
transform: translateY(-2px); transform: translateY(-1px);
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); }
} }
.admin-link { .admin-link {
font-size: 14px; font-size: 14px;
color: #fff; color: #6c757d;
text-decoration: none; text-decoration: none;
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.05);
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 8px; border-radius: 8px;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.admin-link:hover { .admin-link:hover {
background: rgba(0, 0, 0, 0.4); background: rgba(0, 0, 0, 0.1);
text-decoration: none; text-decoration: none;
} }
/* Admin Styles */ /* Admin Styles */
.admin-container { .admin-container {
max-width: 900px; max-width: 900px;
margin: 3rem auto; margin: 2rem auto;
padding: 2.5rem; padding: 2rem;
background: rgba(255, 255, 255, 0.85); background: var(--bs-body-bg);
backdrop-filter: blur(20px); border-radius: 16px;
-webkit-backdrop-filter: blur(20px); box-shadow: 0 5px 20px rgba(0,0,0,0.05);
border-radius: 24px; border: 1px solid var(--bs-border-color);
box-shadow: 0 20px 50px rgba(0,0,0,0.15);
border: 1px solid rgba(255, 255, 255, 0.4);
position: relative; position: relative;
z-index: 1; z-index: 1;
color: var(--bs-body-color);
} }
.admin-container h1 { .admin-container h1 {
margin-top: 0; margin-top: 0;
color: #212529; color: var(--bs-heading-color, var(--bs-body-color)); /* Use variable or fallback */
font-weight: 800; font-weight: 700;
} }
.table { .table {
@ -252,13 +397,14 @@ body {
border-collapse: separate; border-collapse: separate;
border-spacing: 0 8px; border-spacing: 0 8px;
margin-top: 1.5rem; margin-top: 1.5rem;
color: var(--bs-body-color);
} }
.table th { .table th {
background: transparent; background: transparent;
border: none; border: none;
padding: 1rem; padding: 1rem;
color: #6c757d; color: #6c757d; /* Keep subtle for headers, but overridden for dark themes */
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
font-size: 0.75rem; font-size: 0.75rem;
@ -266,13 +412,15 @@ body {
} }
.table td { .table td {
background: #fff; background: var(--bs-body-bg);
padding: 1rem; 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:first-child { border-left: 1px solid var(--bs-border-color); border-radius: 8px 0 0 8px; }
.table tr td:last-child { border-radius: 0 12px 12px 0; } .table tr td:last-child { border-right: 1px solid var(--bs-border-color); border-radius: 0 8px 8px 0; }
.form-group { .form-group {
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
@ -283,20 +431,94 @@ body {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
font-weight: 600; font-weight: 600;
font-size: 0.9rem; font-size: 0.9rem;
color: var(--bs-body-color);
} }
.form-control { .form-control {
width: 100%; width: 100%;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid var(--bs-border-color);
border-radius: 12px; border-radius: 8px;
background: #fff; background: var(--bs-body-bg);
color: var(--bs-body-color);
transition: all 0.3s ease; transition: all 0.3s ease;
box-sizing: border-box; box-sizing: border-box;
} }
.form-control:focus { .form-control:focus {
outline: none; outline: none;
border-color: #23a6d5; border-color: #0d6efd;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); 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 */

View File

@ -3,6 +3,10 @@ document.addEventListener('DOMContentLoaded', () => {
const chatInput = document.getElementById('chat-input'); const chatInput = document.getElementById('chat-input');
const chatMessages = document.getElementById('chat-messages'); const chatMessages = document.getElementById('chat-messages');
if (!chatForm || !chatInput || !chatMessages) {
return; // Not on the chat page
}
const appendMessage = (text, sender) => { const appendMessage = (text, sender) => {
const msgDiv = document.createElement('div'); const msgDiv = document.createElement('div');
msgDiv.classList.add('message', sender); msgDiv.classList.add('message', sender);

47
balance_sheet.php Normal file
View 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
View 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'; ?>

View File

@ -15,3 +15,37 @@ function db() {
} }
return $pdo; 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
View File

@ -0,0 +1,4 @@
<?php
// db/migrate.php
// Wrapper to run migrations from db/migrations/migrate.php
require_once __DIR__ . '/migrations/migrate.php';

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

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

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

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

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

View 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);

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

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

View File

@ -0,0 +1,2 @@
-- Migration: Add theme to users
ALTER TABLE users ADD COLUMN theme VARCHAR(20) DEFAULT 'light';

View 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';

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

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

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

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

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

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

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

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

View 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
);

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

View 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);

View File

@ -0,0 +1,53 @@
-- Expenses module tables
CREATE TABLE IF NOT EXISTS expense_categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS expenses (
id INT AUTO_INCREMENT PRIMARY KEY,
date DATE NOT NULL,
category_id INT NOT NULL,
amount DECIMAL(15,2) NOT NULL,
reference VARCHAR(100),
description TEXT,
vendor VARCHAR(100),
payment_method ENUM('Cash', 'Bank Transfer', 'Credit Card', 'Check', 'Other') DEFAULT 'Cash',
receipt_file VARCHAR(255),
user_id INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES expense_categories(id) ON DELETE RESTRICT
);
-- Seed default categories
INSERT INTO expense_categories (name, description) VALUES
('Office Supplies', 'Pens, paper, toner, etc.'),
('Travel', 'Flights, hotels, transport'),
('Meals & Entertainment', 'Client lunches, team events'),
('Utilities', 'Electricity, water, internet'),
('Rent', 'Office rent'),
('Salaries', 'Employee salaries'),
('Maintenance', 'Repairs and maintenance'),
('Advertising', 'Marketing and ads'),
('Software', 'Subscriptions and licenses'),
('Other', 'Miscellaneous expenses');
-- Add permissions
INSERT IGNORE INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete)
SELECT id, 'expenses',
IF(role = 'admin', 1, 0),
IF(role = 'admin', 1, 0),
IF(role = 'admin', 1, 0),
IF(role = 'admin', 1, 0)
FROM users;
INSERT IGNORE INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete)
SELECT id, 'expense_settings',
IF(role = 'admin', 1, 0),
IF(role = 'admin', 1, 0),
IF(role = 'admin', 1, 0),
IF(role = 'admin', 1, 0)
FROM users;

View 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';

View 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 '%تسو%';

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

View File

@ -0,0 +1,2 @@
-- Fix accounting_accounts type column length
ALTER TABLE accounting_accounts MODIFY type VARCHAR(100) NOT NULL;

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

View 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
View 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
View 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
View File

@ -0,0 +1,199 @@
<?php
require_once __DIR__ . '/includes/header.php';
if (!canView('expenses')) {
redirect('index.php');
}
// Params
$date_from = $_GET['date_from'] ?? date('Y-m-01');
$date_to = $_GET['date_to'] ?? date('Y-m-t');
$category_id = $_GET['category_id'] ?? '';
$vendor = $_GET['vendor'] ?? '';
$payment_method = $_GET['payment_method'] ?? '';
// Build Query
$sql = "SELECT e.*, c.name as category_name, u.username as created_by
FROM expenses e
LEFT JOIN expense_categories c ON e.category_id = c.id
LEFT JOIN users u ON e.user_id = u.id
WHERE e.date BETWEEN ? AND ?";
$params = [$date_from, $date_to];
if ($category_id) {
$sql .= " AND e.category_id = ?";
$params[] = $category_id;
}
if ($vendor) {
$sql .= " AND e.vendor LIKE ?";
$params[] = "%$vendor%";
}
if ($payment_method) {
$sql .= " AND e.payment_method = ?";
$params[] = $payment_method;
}
$sql .= " ORDER BY e.date ASC";
$stmt = db()->prepare($sql);
$stmt->execute($params);
$expenses = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Calculate Totals
$total_amount = 0;
$category_breakdown = [];
foreach ($expenses as $exp) {
$total_amount += $exp['amount'];
if (!isset($category_breakdown[$exp['category_name']])) {
$category_breakdown[$exp['category_name']] = 0;
}
$category_breakdown[$exp['category_name']] += $exp['amount'];
}
// Fetch Categories
$categories = db()->query("SELECT * FROM expense_categories ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
?>
<style>
@media print {
.no-print { display: none !important; }
.sidebar, .top-navbar { display: none !important; }
.main-content { margin: 0 !important; padding: 0 !important; }
.card { border: none !important; shadow: none !important; }
body { background: white !important; }
}
</style>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom no-print">
<h1 class="h2">تقارير المصروفات</h1>
<button onclick="window.print()" class="btn btn-outline-secondary">
<i class="fas fa-print"></i> طباعة التقرير
</button>
</div>
<!-- Filters -->
<div class="card shadow-sm border-0 mb-4 no-print">
<div class="card-body bg-light">
<form method="GET" class="row g-3">
<div class="col-md-3">
<label class="form-label">من تاريخ</label>
<input type="date" name="date_from" class="form-control" value="<?= $date_from ?>">
</div>
<div class="col-md-3">
<label class="form-label">إلى تاريخ</label>
<input type="date" name="date_to" class="form-control" value="<?= $date_to ?>">
</div>
<div class="col-md-3">
<label class="form-label">التصنيف</label>
<select name="category_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>" <?= $category_id == $cat['id'] ? 'selected' : '' ?>><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label">طريقة الدفع</label>
<select name="payment_method" class="form-select">
<option value="">الكل</option>
<option value="Cash" <?= $payment_method == 'Cash' ? 'selected' : '' ?>>نقد</option>
<option value="Bank Transfer" <?= $payment_method == 'Bank Transfer' ? 'selected' : '' ?>>تحويل بنكي</option>
<option value="Credit Card" <?= $payment_method == 'Credit Card' ? 'selected' : '' ?>>بطاقة ائتمان</option>
<option value="Check" <?= $payment_method == 'Check' ? 'selected' : '' ?>>شيك</option>
</select>
</div>
<div class="col-md-12 text-end">
<button type="submit" class="btn btn-primary px-4"><i class="fas fa-filter me-1"></i> عرض التقرير</button>
</div>
</form>
</div>
</div>
<!-- Report Header (Print Only) -->
<div class="d-none d-print-block text-center mb-4">
<h3>تقرير المصروفات التفصيلي</h3>
<p class="text-muted">الفترة من <?= $date_from ?> إلى <?= $date_to ?></p>
</div>
<!-- Summary Cards -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card shadow-sm border-0 h-100 bg-primary text-white">
<div class="card-body text-center">
<h6 class="text-white-50 mb-2">إجمالي المصروفات</h6>
<h2 class="fw-bold mb-0"><?= number_format($total_amount, 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
View 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
View File

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

145
forgot_password.php Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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'; ?>

View 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
View 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">
&copy; <?= 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
View 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">
&copy; <?= 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
View 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
View 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
View 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
View File

@ -1,150 +1,333 @@
<?php <?php
declare(strict_types=1); require_once __DIR__ . '/includes/header.php';
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION; // Role-based routing: Admins stay here, others go to their dashboard
$now = date('Y-m-d H:i:s'); 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"> <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<head> <h1 class="h2">لوحة التحكم الإدارية</h1>
<meta charset="utf-8" /> <div class="btn-toolbar mb-2 mb-md-0">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <div class="btn-group me-2">
<title>New Style</title> <?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 <?php
// Read project preview data from environment if (canView('reports')):
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? ''; // Combine overdue counts from inbound and outbound
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; $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): ?> <div class="row mb-4">
<!-- Meta description --> <div class="col-12">
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' /> <div class="alert alert-danger shadow-sm border-0 d-flex align-items-center justify-content-between mb-0">
<!-- Open Graph meta tags --> <div>
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" /> <i class="fas fa-exclamation-triangle fs-4 me-3"></i>
<!-- Twitter meta tags --> <span class="fw-bold">هناك <?= $overdue_count ?> مهام متأخرة تتطلب انتباهك!</span>
<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> </div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p> <a href="overdue_report.php" class="btn btn-danger btn-sm">عرض التقرير</a>
<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>
</div> </div>
</main> </div>
<footer> </div>
Page updated: <?= htmlspecialchars($now) ?> (UTC) <?php
</footer> endif;
</body> endif;
</html> ?>
<!-- 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
<?php
session_start();
session_unset();
session_destroy();
header("Location: login.php");
exit;

191
m_services/MailService.php Normal file
View 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
View 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
View 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
View 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>

View File

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

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 "تمت إضافة الحسابات الشائعة بنجاح.";

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
<?php echo "Test file works!"; ?>

50
trial_balance.php Normal file
View 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'; ?>

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

324
user_dashboard.php Normal file
View 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
View 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
View 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'; ?>