Autosave: 20260325-100805

This commit is contained in:
Flatlogic Bot 2026-03-25 10:08:05 +00:00
parent f72c5f11b8
commit ba0567e218
9 changed files with 1674 additions and 559 deletions

649
admin.php
View File

@ -1,572 +1,129 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/admin_layout.php';
library_bootstrap();
$errors = [];
$successMessage = '';
// Handle POST request
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
try {
if ($action === 'upload_document') {
$documentId = library_create_document($_POST, $_FILES['document_file'] ?? []);
library_set_flash('success', 'Document uploaded successfully.');
header('Location: /admin.php?created=' . $documentId . '#catalog-manager');
exit;
} elseif ($action === 'create_category') {
$nameEn = trim($_POST['name_en'] ?? '');
$nameAr = trim($_POST['name_ar'] ?? '');
if (!$nameEn || !$nameAr) {
throw new RuntimeException('Both English and Arabic names are required for Category.');
}
library_create_category($nameEn, $nameAr);
library_set_flash('success', 'Category created successfully.');
header('Location: /admin.php');
exit;
} elseif ($action === 'update_category') {
$nameEn = trim($_POST['name_en'] ?? '');
$nameAr = trim($_POST['name_ar'] ?? '');
if (!$id || !$nameEn || !$nameAr) {
throw new RuntimeException('ID, English name, and Arabic name are required.');
}
library_update_category($id, $nameEn, $nameAr);
library_set_flash('success', 'Category updated successfully.');
header('Location: /admin.php');
exit;
} elseif ($action === 'delete_category') {
if (!$id) {
throw new RuntimeException('Invalid Category ID.');
}
library_delete_category($id);
library_set_flash('success', 'Category deleted successfully.');
header('Location: /admin.php');
exit;
} elseif ($action === 'create_subcategory') {
$catId = (int)($_POST['category_id'] ?? 0);
$nameEn = trim($_POST['name_en'] ?? '');
$nameAr = trim($_POST['name_ar'] ?? '');
if (!$catId || !$nameEn || !$nameAr) {
throw new RuntimeException('Category, English name, and Arabic name are required.');
}
library_create_subcategory($catId, $nameEn, $nameAr);
library_set_flash('success', 'Subcategory created successfully.');
header('Location: /admin.php');
exit;
} elseif ($action === 'update_subcategory') {
$catId = (int)($_POST['category_id'] ?? 0);
$nameEn = trim($_POST['name_en'] ?? '');
$nameAr = trim($_POST['name_ar'] ?? '');
if (!$id || !$catId || !$nameEn || !$nameAr) {
throw new RuntimeException('ID, Category, English name, and Arabic name are required.');
}
library_update_subcategory($id, $catId, $nameEn, $nameAr);
library_set_flash('success', 'Subcategory updated successfully.');
header('Location: /admin.php');
exit;
} elseif ($action === 'delete_subcategory') {
if (!$id) {
throw new RuntimeException('Invalid Subcategory ID.');
}
library_delete_subcategory($id);
library_set_flash('success', 'Subcategory deleted successfully.');
header('Location: /admin.php');
exit;
}
} catch (Throwable $exception) {
$errors[] = $exception->getMessage();
}
}
$documents = library_fetch_documents(false, []);
$metrics = library_catalog_metrics();
$categories = library_get_categories();
// Fetch all subcategories to pass to JS for filtering
$allSubcategories = library_get_subcategories(null);
$flashes = library_get_flashes();
admin_render_header('Dashboard', 'dashboard');
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Admin Studio · Nabd Library</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
.admin-sidebar {
width: 260px;
height: 100vh;
position: sticky;
top: 0;
background: #fff;
border-right: 1px solid #dee2e6;
padding: 20px;
}
</style>
</head>
<body class="bg-light">
<div class="d-flex">
<aside class="admin-sidebar">
<h4 class="mb-4">Admin Dashboard</h4>
<nav class="nav flex-column gap-2">
<a class="nav-link text-dark bg-light rounded" href="/admin.php">Catalog Manager</a>
<a class="nav-link text-secondary" href="/index.php">Return to site</a>
</nav>
<hr class="my-4">
<h6 class="text-uppercase text-muted small">Metadata</h6>
<div class="d-grid gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm text-start" onclick="openCreateCategoryModal()">
+ New Category
</button>
<button type="button" class="btn btn-outline-secondary btn-sm text-start" onclick="openCreateSubcategoryModal()">
+ New Subcategory
</button>
</div>
</aside>
<main class="flex-grow-1 p-4">
<h1>Catalog Manager</h1>
<p class="text-secondary">Upload manuscripts and manage permissions.</p>
<?php foreach ($flashes as $flash): ?>
<div class="alert alert-<?= $flash['type'] === 'error' ? 'danger' : 'success' ?> alert-dismissible fade show" role="alert">
<?= h($flash['message']) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<!-- Metrics -->
<div class="row g-4 mb-5">
<div class="col-md-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="text-uppercase text-secondary small fw-bold">Total Documents</h6>
<div class="d-flex align-items-center justify-content-between mt-3">
<h2 class="display-6 fw-bold mb-0 text-primary"><?= number_format($metrics['total_documents']) ?></h2>
<i class="bi bi-file-earmark-text fs-1 text-primary opacity-25"></i>
</div>
</div>
<?php endforeach; ?>
<?php if ($errors): ?>
<div class="alert alert-danger"><?= h(implode(' ', $errors)) ?></div>
<?php endif; ?>
<form method="post" action="/admin.php#catalog-manager" enctype="multipart/form-data" class="panel border p-4 bg-white rounded shadow-sm mb-5">
<input type="hidden" name="action" value="upload_document">
<h4 class="mb-3">Upload New Document</h4>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Title (English)</label>
<input class="form-control" name="title_en" type="text" required>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="text-uppercase text-secondary small fw-bold">Public Titles</h6>
<div class="d-flex align-items-center justify-content-between mt-3">
<h2 class="display-6 fw-bold mb-0 text-success"><?= number_format($metrics['public_documents']) ?></h2>
<i class="bi bi-globe fs-1 text-success opacity-25"></i>
</div>
<div class="col-md-6">
<label class="form-label">Title (Arabic)</label>
<input class="form-control" name="title_ar" type="text" dir="rtl">
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="text-uppercase text-secondary small fw-bold">Total Downloads</h6>
<div class="d-flex align-items-center justify-content-between mt-3">
<h2 class="display-6 fw-bold mb-0 text-info"><?= number_format($metrics['total_downloads']) ?></h2>
<i class="bi bi-download fs-1 text-info opacity-25"></i>
</div>
<div class="col-md-6">
<label class="form-label">Category</label>
<div class="input-group">
<select class="form-select" name="category_id" id="docCategorySelect" required>
<option value="">Select Category...</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>">
<?= h($cat['name_en']) ?> / <?= h($cat['name_ar']) ?>
</option>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Recent Documents -->
<div class="col-lg-8">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3 border-bottom">
<h5 class="card-title mb-0">Recent Documents</h5>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-4">Title / Author</th>
<th>Type / Category</th>
<th>Visibility</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($documents)): ?>
<tr><td colspan="4" class="text-center py-5 text-muted">No documents found.</td></tr>
<?php else: ?>
<?php foreach (array_slice($documents, 0, 10) as $doc): ?>
<tr>
<td class="ps-4">
<div class="fw-medium text-dark"><?= h($doc['title_en']) ?></div>
<div class="small text-muted" dir="rtl"><?= h($doc['title_ar']) ?></div>
<?php if (!empty($doc['author'])): ?>
<div class="small text-primary mt-1"><i class="bi bi-person me-1"></i><?= h($doc['author']) ?></div>
<?php endif; ?>
</td>
<td>
<?php if (!empty($doc['type_en'])): ?>
<span class="badge bg-info bg-opacity-10 text-info border border-info border-opacity-25 mb-1"><?= h($doc['type_en']) ?></span><br>
<?php endif; ?>
<span class="badge bg-light text-dark border">
<?= h($doc['cat_en'] ?? $doc['category']) ?>
</span>
</td>
<td>
<span class="badge bg-<?= $doc['visibility'] === 'public' ? 'success' : 'secondary' ?>">
<?= h($doc['visibility']) ?>
</span>
</td>
<td class="text-end pe-4">
<a href="/document.php?id=<?= $doc['id'] ?>" class="btn btn-sm btn-outline-primary" target="_blank">
<i class="bi bi-eye"></i> View
</a>
</td>
</tr>
<?php endforeach; ?>
</select>
<button class="btn btn-outline-secondary" type="button" onclick="openCreateCategoryModal()">+</button>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Sub Category</label>
<div class="input-group">
<select class="form-select" name="subcategory_id" id="docSubcategorySelect">
<option value="">Select Category First</option>
</select>
<button class="btn btn-outline-secondary" type="button" onclick="openCreateSubcategoryModal()">+</button>
</div>
</div>
<div class="col-md-12">
<label class="form-label">Visibility</label>
<select class="form-select" name="visibility">
<option value="public">Public</option>
<option value="private">Private</option>
</select>
</div>
<div class="col-12">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="allow_download" value="1">
<label class="form-check-label">Allow Download</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="allow_print" value="1">
<label class="form-check-label">Allow Print</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="allow_copy" value="1">
<label class="form-check-label">Allow Copy</label>
</div>
</div>
<div class="col-12">
<label class="form-label">Document file</label>
<input class="form-control" name="document_file" type="file" required>
</div>
<?php endif; ?>
</tbody>
</table>
</div>
<button class="btn btn-primary mt-3" type="submit">Upload manuscript</button>
</form>
<h3 class="mb-3">Recent Documents</h3>
<div class="table-responsive bg-white border rounded mb-5">
<table class="table mb-0">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Title</th>
<th>Category</th>
<th>Visibility</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($documents)): ?>
<tr><td colspan="5" class="text-center py-4 text-muted">No documents found.</td></tr>
<?php else: ?>
<?php foreach (array_slice($documents, 0, 10) as $doc): ?>
<tr>
<td><?= $doc['id'] ?></td>
<td>
<?= h($doc['title_en']) ?><br>
<small class="text-muted"><?= h($doc['title_ar']) ?></small>
</td>
<td>
<?= h($doc['cat_en'] ?? $doc['category']) ?>
<?php if (!empty($doc['sub_en'])): ?>
<small class="text-muted"> > <?= h($doc['sub_en']) ?></small>
<?php elseif (!empty($doc['sub_category'])): ?>
<small class="text-muted"> > <?= h($doc['sub_category']) ?></small>
<?php endif; ?>
</td>
<td>
<span class="badge bg-<?= $doc['visibility'] === 'public' ? 'success' : 'secondary' ?>">
<?= h($doc['visibility']) ?>
</span>
</td>
<td>
<a href="/document.php?id=<?= $doc['id'] ?>" class="btn btn-sm btn-outline-primary" target="_blank">View</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="row">
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3>Categories</h3>
<button class="btn btn-sm btn-outline-primary" onclick="openCreateCategoryModal()">+ Add New</button>
</div>
<div class="table-responsive bg-white border rounded">
<table class="table mb-0">
<thead class="table-light">
<tr>
<th>Name</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($categories)): ?>
<tr><td colspan="2" class="text-center py-4 text-muted">No categories.</td></tr>
<?php else: ?>
<?php foreach ($categories as $cat): ?>
<tr>
<td>
<?= h($cat['name_en']) ?><br>
<small class="text-muted"><?= h($cat['name_ar']) ?></small>
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-secondary" onclick="openEditCategoryModal(<?= $cat['id'] ?>, '<?= h($cat['name_en']) ?>', '<?= h($cat['name_ar']) ?>')">Edit</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteCategory(<?= $cat['id'] ?>)">Delete</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3>Subcategories</h3>
<button class="btn btn-sm btn-outline-primary" onclick="openCreateSubcategoryModal()">+ Add New</button>
</div>
<div class="table-responsive bg-white border rounded">
<table class="table mb-0">
<thead class="table-light">
<tr>
<th>Name</th>
<th>Parent</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($allSubcategories)): ?>
<tr><td colspan="3" class="text-center py-4 text-muted">No subcategories.</td></tr>
<?php else: ?>
<?php foreach ($allSubcategories as $sub):
$parentName = 'Unknown';
foreach ($categories as $c) {
if ($c['id'] == $sub['category_id']) {
$parentName = $c['name_en'];
break;
}
}
?>
<tr>
<td>
<?= h($sub['name_en']) ?><br>
<small class="text-muted"><?= h($sub['name_ar']) ?></small>
</td>
<td><small><?= h($parentName) ?></small></td>
<td class="text-end">
<button class="btn btn-sm btn-outline-secondary" onclick="openEditSubcategoryModal(<?= $sub['id'] ?>, <?= $sub['category_id'] ?>, '<?= h($sub['name_en']) ?>', '<?= h($sub['name_ar']) ?>')">Edit</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteSubcategory(<?= $sub['id'] ?>)">Delete</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="card-footer bg-white text-center py-3">
<small class="text-muted">Showing last 10 uploads</small>
</div>
</div>
</main>
</div>
<!-- Category Modal -->
<div class="modal fade" id="categoryModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="/admin.php" id="categoryForm">
<input type="hidden" name="action" id="cat_action" value="create_category">
<input type="hidden" name="id" id="cat_id" value="">
<div class="modal-header">
<h5 class="modal-title" id="categoryModalTitle">Add New Category</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Name (English)</label>
<div class="input-group">
<input type="text" class="form-control" name="name_en" id="cat_name_en" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('cat_name_en', 'cat_name_ar', 'Arabic')" title="Translate to Arabic">
<i class="bi bi-translate"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">Name (Arabic)</label>
<div class="input-group">
<input type="text" class="form-control" name="name_ar" id="cat_name_ar" dir="rtl" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('cat_name_ar', 'cat_name_en', 'English')" title="Translate to English">
<i class="bi bi-translate"></i>
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
</div>
<!-- Subcategory Modal -->
<div class="modal fade" id="subcategoryModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="/admin.php" id="subcategoryForm">
<input type="hidden" name="action" id="sub_action" value="create_subcategory">
<input type="hidden" name="id" id="sub_id" value="">
<div class="modal-header">
<h5 class="modal-title" id="subcategoryModalTitle">Add New Subcategory</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Parent Category</label>
<select class="form-select" name="category_id" id="sub_category_id" required>
<option value="">Select...</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>"><?= h($cat['name_en']) ?></option>
<?php endforeach; ?>
</select>
<!-- Material Entry Promo -->
<div class="col-lg-4">
<div class="card shadow-sm border-0">
<div class="card-header bg-primary text-white py-3">
<h5 class="card-title mb-0"><i class="bi bi-folder2-open me-2"></i>Material Entry</h5>
</div>
<div class="mb-3">
<label class="form-label">Name (English)</label>
<div class="input-group">
<input type="text" class="form-control" name="name_en" id="sub_name_en" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('sub_name_en', 'sub_name_ar', 'Arabic')" title="Translate to Arabic">
<i class="bi bi-translate"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">Name (Arabic)</label>
<div class="input-group">
<input type="text" class="form-control" name="name_ar" id="sub_name_ar" dir="rtl" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('sub_name_ar', 'sub_name_en', 'English')" title="Translate to English">
<i class="bi bi-translate"></i>
</button>
</div>
<div class="card-body p-4 text-center">
<p class="text-secondary mb-4">
Manage your library documents, upload new materials, and edit metadata including bilingual titles, summaries, and more.
</p>
<a href="/admin_documents.php" class="btn btn-primary w-100">
<i class="bi bi-plus-lg me-2"></i> Manage Documents
</a>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Confirmation Form (Hidden) -->
<form method="post" action="/admin.php" id="deleteForm">
<input type="hidden" name="action" id="deleteAction" value="">
<input type="hidden" name="id" id="deleteId" value="">
</form>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Pass PHP data to JS safely
const allSubcategories = <?= json_encode($allSubcategories) ?>;
const catSelect = document.getElementById('docCategorySelect');
const subSelect = document.getElementById('docSubcategorySelect');
const categoryModal = new bootstrap.Modal(document.getElementById('categoryModal'));
const subcategoryModal = new bootstrap.Modal(document.getElementById('subcategoryModal'));
function updateSubcategories() {
const selectedCatId = catSelect.value;
subSelect.innerHTML = '<option value="">Select Subcategory...</option>';
if (!selectedCatId) {
return;
}
// Loose equality check for ID comparison
const filtered = allSubcategories.filter(sub => sub.category_id == selectedCatId);
filtered.forEach(sub => {
const option = document.createElement('option');
option.value = sub.id;
option.textContent = sub.name_en + ' / ' + sub.name_ar;
subSelect.appendChild(option);
});
}
if (catSelect && subSelect) {
catSelect.addEventListener('change', updateSubcategories);
updateSubcategories();
}
// Modal Helpers
function openCreateCategoryModal() {
document.getElementById('categoryModalTitle').innerText = 'Add New Category';
document.getElementById('cat_action').value = 'create_category';
document.getElementById('cat_id').value = '';
document.getElementById('cat_name_en').value = '';
document.getElementById('cat_name_ar').value = '';
categoryModal.show();
}
function openEditCategoryModal(id, nameEn, nameAr) {
document.getElementById('categoryModalTitle').innerText = 'Edit Category';
document.getElementById('cat_action').value = 'update_category';
document.getElementById('cat_id').value = id;
document.getElementById('cat_name_en').value = nameEn;
document.getElementById('cat_name_ar').value = nameAr;
categoryModal.show();
}
function deleteCategory(id) {
if (confirm('Are you sure you want to delete this category? All related subcategories will also be deleted.')) {
document.getElementById('deleteAction').value = 'delete_category';
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();
}
}
function openCreateSubcategoryModal() {
document.getElementById('subcategoryModalTitle').innerText = 'Add New Subcategory';
document.getElementById('sub_action').value = 'create_subcategory';
document.getElementById('sub_id').value = '';
document.getElementById('sub_category_id').value = '';
document.getElementById('sub_name_en').value = '';
document.getElementById('sub_name_ar').value = '';
subcategoryModal.show();
}
function openEditSubcategoryModal(id, catId, nameEn, nameAr) {
document.getElementById('subcategoryModalTitle').innerText = 'Edit Subcategory';
document.getElementById('sub_action').value = 'update_subcategory';
document.getElementById('sub_id').value = id;
document.getElementById('sub_category_id').value = catId;
document.getElementById('sub_name_en').value = nameEn;
document.getElementById('sub_name_ar').value = nameAr;
subcategoryModal.show();
}
function deleteSubcategory(id) {
if (confirm('Are you sure you want to delete this subcategory?')) {
document.getElementById('deleteAction').value = 'delete_subcategory';
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();
}
}
async function translateText(sourceId, targetId, lang) {
const source = document.getElementById(sourceId);
const target = document.getElementById(targetId);
const text = source.value.trim();
if (!text) {
alert('Please enter text to translate.');
return;
}
const originalPlaceholder = target.placeholder;
target.placeholder = 'Translating...';
const originalOpacity = target.style.opacity;
target.style.opacity = '0.7';
try {
const response = await fetch('/api/translate.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text, target_lang: lang })
});
if (!response.ok) throw new Error('Translation failed');
const data = await response.json();
if (data.translation) {
target.value = data.translation;
} else if (data.error) {
alert('Translation error: ' + data.error);
}
} catch (e) {
console.error(e);
alert('Translation failed. Please try again.');
} finally {
target.placeholder = originalPlaceholder;
target.style.opacity = originalOpacity;
}
}
</script>
</body>
</html>
<?php admin_render_footer(); ?>

212
admin_categories.php Normal file
View File

@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/admin_layout.php';
library_bootstrap();
$errors = [];
// Handle POST request
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
try {
if ($action === 'create_category') {
$nameEn = trim($_POST['name_en'] ?? '');
$nameAr = trim($_POST['name_ar'] ?? '');
if (!$nameEn || !$nameAr) {
throw new RuntimeException('Both English and Arabic names are required for Category.');
}
library_create_category($nameEn, $nameAr);
library_set_flash('success', 'Category created successfully.');
header('Location: /admin_categories.php');
exit;
} elseif ($action === 'update_category') {
$nameEn = trim($_POST['name_en'] ?? '');
$nameAr = trim($_POST['name_ar'] ?? '');
if (!$id || !$nameEn || !$nameAr) {
throw new RuntimeException('ID, English name, and Arabic name are required.');
}
library_update_category($id, $nameEn, $nameAr);
library_set_flash('success', 'Category updated successfully.');
header('Location: /admin_categories.php');
exit;
} elseif ($action === 'delete_category') {
if (!$id) {
throw new RuntimeException('Invalid Category ID.');
}
library_delete_category($id);
library_set_flash('success', 'Category deleted successfully.');
header('Location: /admin_categories.php');
exit;
}
} catch (Throwable $exception) {
$errors[] = $exception->getMessage();
}
}
// Search Logic
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$categories = library_get_categories($search);
admin_render_header('Categories', 'categories');
?>
<!-- Page Content -->
<?php if ($errors): ?>
<div class="alert alert-danger"><?= h(implode(' ', $errors)) ?></div>
<?php endif; ?>
<div class="d-flex justify-content-between align-items-center mb-4">
<p class="text-secondary mb-0">Manage document categories.</p>
<button class="btn btn-primary" onclick="openCreateCategoryModal()">
<i class="bi bi-plus-lg me-1"></i> Add New Category
</button>
</div>
<!-- Search Bar -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<form method="get" action="/admin_categories.php" class="row g-2 align-items-center">
<div class="col-auto flex-grow-1">
<input type="text" name="search" class="form-control" placeholder="Search categories..." value="<?= h($search) ?>">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-outline-primary">
<i class="bi bi-search"></i> Search
</button>
<?php if ($search): ?>
<a href="/admin_categories.php" class="btn btn-outline-secondary">Clear</a>
<?php endif; ?>
</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="table-light">
<tr>
<th class="ps-4">Name</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($categories)): ?>
<tr><td colspan="2" class="text-center py-5 text-muted">No categories found.</td></tr>
<?php else: ?>
<?php foreach ($categories as $cat): ?>
<tr>
<td class="ps-4">
<div class="fw-medium text-dark"><?= h($cat['name_en']) ?></div>
<small class="text-muted"><?= h($cat['name_ar']) ?></small>
</td>
<td class="text-end pe-4">
<button class="btn btn-sm btn-outline-secondary me-1"
data-id="<?= $cat['id'] ?>"
data-name-en="<?= h($cat['name_en']) ?>"
data-name-ar="<?= h($cat['name_ar']) ?>"
onclick="openEditCategoryModal(this)">
<i class="bi bi-pencil"></i> Edit
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteCategory(<?= $cat['id'] ?>)">
<i class="bi bi-trash"></i> Delete
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Category Modal -->
<div class="modal fade" id="categoryModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="/admin_categories.php" id="categoryForm">
<input type="hidden" name="action" id="cat_action" value="create_category">
<input type="hidden" name="id" id="cat_id" value="">
<div class="modal-header">
<h5 class="modal-title" id="categoryModalTitle">Add New Category</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Name (English)</label>
<div class="input-group">
<input type="text" class="form-control" name="name_en" id="cat_name_en" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('cat_name_en', 'cat_name_ar', 'Arabic')" title="Translate to Arabic">
<i class="bi bi-translate"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">Name (Arabic)</label>
<div class="input-group">
<input type="text" class="form-control" name="name_ar" id="cat_name_ar" dir="rtl" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('cat_name_ar', 'cat_name_en', 'English')" title="Translate to English">
<i class="bi bi-translate"></i>
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Confirmation Form -->
<form method="post" action="/admin_categories.php" id="deleteForm">
<input type="hidden" name="action" id="deleteAction" value="">
<input type="hidden" name="id" id="deleteId" value="">
</form>
<script>
let categoryModal;
document.addEventListener('DOMContentLoaded', function() {
categoryModal = new bootstrap.Modal(document.getElementById('categoryModal'));
});
function openCreateCategoryModal() {
document.getElementById('categoryModalTitle').innerText = 'Add New Category';
document.getElementById('cat_action').value = 'create_category';
document.getElementById('cat_id').value = '';
document.getElementById('cat_name_en').value = '';
document.getElementById('cat_name_ar').value = '';
categoryModal.show();
}
function openEditCategoryModal(btn) {
const id = btn.getAttribute('data-id');
const nameEn = btn.getAttribute('data-name-en');
const nameAr = btn.getAttribute('data-name-ar');
document.getElementById('categoryModalTitle').innerText = 'Edit Category';
document.getElementById('cat_action').value = 'update_category';
document.getElementById('cat_id').value = id;
document.getElementById('cat_name_en').value = nameEn;
document.getElementById('cat_name_ar').value = nameAr;
categoryModal.show();
}
function deleteCategory(id) {
if (confirm('Are you sure you want to delete this category? All related subcategories will also be deleted.')) {
document.getElementById('deleteAction').value = 'delete_category';
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();
}
}
</script>
<?php admin_render_footer(); ?>

423
admin_documents.php Normal file
View File

@ -0,0 +1,423 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/admin_layout.php';
library_bootstrap();
$errors = [];
// Handle POST request
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
try {
if ($action === 'create_document') {
library_create_document($_POST, $_FILES['document_file'] ?? [], $_FILES['cover_file'] ?? []);
library_set_flash('success', 'Document created successfully.');
header('Location: /admin_documents.php');
exit;
} elseif ($action === 'update_document') {
if (!$id) {
throw new RuntimeException('Invalid Document ID.');
}
library_update_document($id, $_POST, $_FILES['document_file'] ?? [], $_FILES['cover_file'] ?? []);
library_set_flash('success', 'Document updated successfully.');
header('Location: /admin_documents.php');
exit;
} elseif ($action === 'delete_document') {
if (!$id) {
throw new RuntimeException('Invalid Document ID.');
}
library_delete_document($id);
library_set_flash('success', 'Document deleted successfully.');
header('Location: /admin_documents.php');
exit;
}
} catch (Throwable $exception) {
$errors[] = $exception->getMessage();
}
}
// Search Logic
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
// We can implement search in fetch_documents if needed, currently it supports filters
// For now, fetch all and filter in PHP or improve SQL later if specific search needed.
// library_fetch_documents doesn't have search param yet, let's just fetch all.
$documents = library_fetch_documents(false);
// Basic search filter in PHP for now
if ($search !== '') {
$documents = array_filter($documents, function($doc) use ($search) {
return stripos($doc['title_en'] ?? '', $search) !== false
|| stripos($doc['title_ar'] ?? '', $search) !== false
|| stripos($doc['author'] ?? '', $search) !== false;
});
}
$categories = library_get_categories();
$allSubcategories = library_get_subcategories(null);
$types = library_get_types();
admin_render_header('Material Entry', 'documents');
?>
<!-- Page Content -->
<?php if ($errors): ?>
<div class="alert alert-danger"><?= h(implode(' ', $errors)) ?></div>
<?php endif; ?>
<div class="d-flex justify-content-between align-items-center mb-4">
<p class="text-secondary mb-0">Manage library documents (Material Entry).</p>
<button class="btn btn-primary" onclick="openCreateModal()">
<i class="bi bi-plus-lg me-1"></i> Add New Document
</button>
</div>
<!-- Search Bar -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<form method="get" action="/admin_documents.php" class="row g-2 align-items-center">
<div class="col-auto flex-grow-1">
<input type="text" name="search" class="form-control" placeholder="Search by title or author..." value="<?= h($search) ?>">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-outline-primary">
<i class="bi bi-search"></i> Search
</button>
<?php if ($search): ?>
<a href="/admin_documents.php" class="btn btn-outline-secondary">Clear</a>
<?php endif; ?>
</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="table-light">
<tr>
<th class="ps-4">ID</th>
<th>Cover</th>
<th>Title / Author</th>
<th>Type / Category</th>
<th>Year</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($documents)): ?>
<tr><td colspan="6" class="text-center py-5 text-muted">No documents found.</td></tr>
<?php else: ?>
<?php foreach ($documents as $doc): ?>
<tr>
<td class="ps-4 text-muted small">#<?= $doc['id'] ?></td>
<td>
<?php if (!empty($doc['cover_image_path'])): ?>
<img src="/<?= h($doc['cover_image_path']) ?>" alt="Cover" class="rounded" style="width: 40px; height: 50px; object-fit: cover;">
<?php else: ?>
<div class="bg-light rounded d-flex align-items-center justify-content-center text-muted small" style="width: 40px; height: 50px;">
<i class="bi bi-image"></i>
</div>
<?php endif; ?>
</td>
<td>
<div class="fw-medium text-dark"><?= h($doc['title_en']) ?></div>
<div class="small text-muted" dir="rtl"><?= h($doc['title_ar']) ?></div>
<?php if (!empty($doc['author'])): ?>
<div class="small text-primary mt-1"><i class="bi bi-person me-1"></i><?= h($doc['author']) ?></div>
<?php endif; ?>
</td>
<td>
<?php if (!empty($doc['type_en'])): ?>
<span class="badge bg-info bg-opacity-10 text-info border border-info border-opacity-25 mb-1"><?= h($doc['type_en']) ?></span><br>
<?php endif; ?>
<span class="badge bg-light text-dark border"><?= h($doc['cat_en'] ?? $doc['category']) ?></span>
<?php if (!empty($doc['sub_en'])): ?>
<i class="bi bi-chevron-right text-muted small"></i>
<span class="badge bg-light text-dark border"><?= h($doc['sub_en']) ?></span>
<?php endif; ?>
</td>
<td><?= h((string)$doc['publish_year']) ?></td>
<td class="text-end pe-4">
<a href="/document.php?id=<?= $doc['id'] ?>" target="_blank" class="btn btn-sm btn-outline-secondary me-1" title="View">
<i class="bi bi-eye"></i>
</a>
<button class="btn btn-sm btn-outline-primary me-1"
onclick='openEditModal(<?= json_encode($doc) ?>)'>
<i class="bi bi-pencil"></i> Edit
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteDocument(<?= $doc['id'] ?>)">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Document Modal -->
<div class="modal fade" id="documentModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<form method="post" action="/admin_documents.php" id="documentForm" enctype="multipart/form-data">
<input type="hidden" name="action" id="doc_action" value="create_document">
<input type="hidden" name="id" id="doc_id" value="">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="documentModalTitle">Add New Material</h5>
<div class="ms-auto">
<button type="button" class="btn btn-link text-white text-decoration-none me-2" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-light text-primary fw-bold">Save Changes</button>
</div>
</div>
<div class="modal-body">
<div class="row g-3">
<!-- Titles -->
<div class="col-md-6">
<label class="form-label small fw-bold text-uppercase text-muted">Title (English)</label>
<div class="input-group">
<input type="text" class="form-control" name="title_en" id="doc_title_en" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('doc_title_en', 'doc_title_ar', 'Arabic')"><i class="bi bi-translate"></i></button>
</div>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-uppercase text-muted">Title (Arabic)</label>
<div class="input-group">
<input type="text" class="form-control" name="title_ar" id="doc_title_ar" dir="rtl">
<button class="btn btn-outline-secondary" type="button" onclick="translateText('doc_title_ar', 'doc_title_en', 'English')"><i class="bi bi-translate"></i></button>
</div>
</div>
<!-- Basic Info -->
<div class="col-md-6">
<label class="form-label small fw-bold text-uppercase text-muted">Author</label>
<input type="text" class="form-control" name="author" id="doc_author">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-uppercase text-muted">Publisher</label>
<input type="text" class="form-control" name="publisher" id="doc_publisher">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-uppercase text-muted">Year</label>
<input type="number" class="form-control" name="publish_year" id="doc_publish_year" min="1000" max="2100">
</div>
<!-- Classification -->
<div class="col-md-4">
<label class="form-label small fw-bold text-uppercase text-muted">Type</label>
<select class="form-select" name="type_id" id="doc_type_id">
<option value="">Select Type...</option>
<?php foreach ($types as $t): ?>
<option value="<?= $t['id'] ?>"><?= h($t['name_en']) ?> / <?= h($t['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold text-uppercase text-muted">Category</label>
<select class="form-select" name="category_id" id="doc_category_id" onchange="updateSubcategories()">
<option value="">Select Category...</option>
<?php foreach ($categories as $c): ?>
<option value="<?= $c['id'] ?>"><?= h($c['name_en']) ?> / <?= h($c['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold text-uppercase text-muted">Subcategory</label>
<select class="form-select" name="subcategory_id" id="doc_subcategory_id">
<option value="">Select Category First...</option>
</select>
</div>
<!-- Details -->
<div class="col-md-6">
<label class="form-label small fw-bold text-uppercase text-muted">Country</label>
<input type="text" class="form-control" name="country" id="doc_country">
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-uppercase text-muted">Total Pages</label>
<input type="number" class="form-control" name="page_count" id="doc_page_count" min="0">
</div>
<!-- Summaries -->
<div class="col-md-6">
<label class="form-label small fw-bold text-uppercase text-muted">Summary (English)</label>
<div class="input-group">
<textarea class="form-control" name="summary_en" id="doc_summary_en" rows="3"></textarea>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('doc_summary_en', 'doc_summary_ar', 'Arabic')"><i class="bi bi-translate"></i></button>
</div>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-uppercase text-muted">Summary (Arabic)</label>
<div class="input-group">
<textarea class="form-control" name="summary_ar" id="doc_summary_ar" rows="3" dir="rtl"></textarea>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('doc_summary_ar', 'doc_summary_en', 'English')"><i class="bi bi-translate"></i></button>
</div>
</div>
<!-- Files -->
<div class="col-md-6">
<label class="form-label small fw-bold text-uppercase text-muted">Front Cover Image</label>
<input class="form-control" type="file" name="cover_file" accept="image/*">
<div id="current_cover_preview" class="mt-2 d-none">
<small class="text-muted d-block mb-1">Current Cover:</small>
<img src="" class="rounded border" style="height: 60px;">
</div>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-uppercase text-muted">Document File</label>
<input class="form-control" type="file" name="document_file">
<div id="current_file_info" class="mt-2 d-none">
<small class="text-muted"><i class="bi bi-file-earmark"></i> <span id="current_filename"></span></small>
</div>
<small class="text-muted d-block mt-1">Leave empty to keep existing file on edit.</small>
</div>
<!-- Settings -->
<div class="col-12">
<div class="card bg-light border-0">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-muted text-uppercase small fw-bold">Visibility & Permissions</h6>
<div class="row">
<div class="col-md-3">
<select class="form-select form-select-sm" name="visibility" id="doc_visibility">
<option value="public">Public</option>
<option value="private">Private</option>
</select>
</div>
<div class="col-md-9 d-flex align-items-center gap-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_download" id="doc_allow_download" value="1">
<label class="form-check-label small" for="doc_allow_download">Download</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_print" id="doc_allow_print" value="1">
<label class="form-check-label small" for="doc_allow_print">Print</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_copy" id="doc_allow_copy" value="1">
<label class="form-check-label small" for="doc_allow_copy">Copy</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Confirmation Form -->
<form method="post" action="/admin_documents.php" id="deleteForm">
<input type="hidden" name="action" id="deleteAction" value="">
<input type="hidden" name="id" id="deleteId" value="">
</form>
<script>
let documentModal;
const allSubcategories = <?= json_encode($allSubcategories) ?>;
document.addEventListener('DOMContentLoaded', function() {
documentModal = new bootstrap.Modal(document.getElementById('documentModal'));
});
function updateSubcategories(selectedSubId = null) {
const catSelect = document.getElementById('doc_category_id');
const subSelect = document.getElementById('doc_subcategory_id');
const catId = catSelect.value;
subSelect.innerHTML = '<option value="">Select Subcategory...</option>';
if (catId) {
const subs = allSubcategories.filter(s => s.category_id == catId);
subs.forEach(s => {
const opt = document.createElement('option');
opt.value = s.id;
opt.textContent = s.name_en + ' / ' + s.name_ar;
if (selectedSubId && s.id == selectedSubId) opt.selected = true;
subSelect.appendChild(opt);
});
}
}
function openCreateModal() {
document.getElementById('documentModalTitle').innerText = 'Add New Material';
document.getElementById('doc_action').value = 'create_document';
document.getElementById('doc_id').value = '';
document.getElementById('documentForm').reset();
// Clear previews
document.getElementById('current_cover_preview').classList.add('d-none');
document.getElementById('current_file_info').classList.add('d-none');
// Reset dynamic selects
updateSubcategories();
documentModal.show();
}
function openEditModal(doc) {
document.getElementById('documentModalTitle').innerText = 'Edit Material: ' + doc.title_en;
document.getElementById('doc_action').value = 'update_document';
document.getElementById('doc_id').value = doc.id;
// Fill fields
document.getElementById('doc_title_en').value = doc.title_en || '';
document.getElementById('doc_title_ar').value = doc.title_ar || '';
document.getElementById('doc_author').value = doc.author || '';
document.getElementById('doc_publisher').value = doc.publisher || '';
document.getElementById('doc_publish_year').value = doc.publish_year || '';
document.getElementById('doc_country').value = doc.country || '';
document.getElementById('doc_page_count').value = doc.page_count || '';
document.getElementById('doc_summary_en').value = doc.summary_en || '';
document.getElementById('doc_summary_ar').value = doc.summary_ar || '';
document.getElementById('doc_type_id').value = doc.type_id || '';
document.getElementById('doc_visibility').value = doc.visibility || 'public';
document.getElementById('doc_allow_download').checked = !!parseInt(doc.allow_download);
document.getElementById('doc_allow_print').checked = !!parseInt(doc.allow_print);
document.getElementById('doc_allow_copy').checked = !!parseInt(doc.allow_copy);
// Handle Category & Subcategory
document.getElementById('doc_category_id').value = doc.category_id || '';
updateSubcategories(doc.subcategory_id);
// Previews
const coverDiv = document.getElementById('current_cover_preview');
if (doc.cover_image_path) {
coverDiv.classList.remove('d-none');
coverDiv.querySelector('img').src = '/' + doc.cover_image_path;
} else {
coverDiv.classList.add('d-none');
}
const fileDiv = document.getElementById('current_file_info');
if (doc.file_name) {
fileDiv.classList.remove('d-none');
document.getElementById('current_filename').innerText = doc.file_name;
} else {
fileDiv.classList.add('d-none');
}
documentModal.show();
}
function deleteDocument(id) {
if (confirm('Are you sure you want to delete this document?')) {
document.getElementById('deleteAction').value = 'delete_document';
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();
}
}
</script>
<?php admin_render_footer(); ?>

240
admin_subcategories.php Normal file
View File

@ -0,0 +1,240 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/admin_layout.php';
library_bootstrap();
$errors = [];
// Handle POST request
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
try {
if ($action === 'create_subcategory') {
$catId = (int)($_POST['category_id'] ?? 0);
$nameEn = trim($_POST['name_en'] ?? '');
$nameAr = trim($_POST['name_ar'] ?? '');
if (!$catId || !$nameEn || !$nameAr) {
throw new RuntimeException('Category, English name, and Arabic name are required.');
}
library_create_subcategory($catId, $nameEn, $nameAr);
library_set_flash('success', 'Subcategory created successfully.');
header('Location: /admin_subcategories.php');
exit;
} elseif ($action === 'update_subcategory') {
$catId = (int)($_POST['category_id'] ?? 0);
$nameEn = trim($_POST['name_en'] ?? '');
$nameAr = trim($_POST['name_ar'] ?? '');
if (!$id || !$catId || !$nameEn || !$nameAr) {
throw new RuntimeException('ID, Category, English name, and Arabic name are required.');
}
library_update_subcategory($id, $catId, $nameEn, $nameAr);
library_set_flash('success', 'Subcategory updated successfully.');
header('Location: /admin_subcategories.php');
exit;
} elseif ($action === 'delete_subcategory') {
if (!$id) {
throw new RuntimeException('Invalid Subcategory ID.');
}
library_delete_subcategory($id);
library_set_flash('success', 'Subcategory deleted successfully.');
header('Location: /admin_subcategories.php');
exit;
}
} catch (Throwable $exception) {
$errors[] = $exception->getMessage();
}
}
// Search Logic
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$categories = library_get_categories();
$allSubcategories = library_get_subcategories(null, $search);
admin_render_header('Subcategories', 'subcategories');
?>
<!-- Page Content -->
<?php if ($errors): ?>
<div class="alert alert-danger"><?= h(implode(' ', $errors)) ?></div>
<?php endif; ?>
<div class="d-flex justify-content-between align-items-center mb-4">
<p class="text-secondary mb-0">Manage document subcategories.</p>
<button class="btn btn-primary" onclick="openCreateSubcategoryModal()">
<i class="bi bi-plus-lg me-1"></i> Add New Subcategory
</button>
</div>
<!-- Search Bar -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<form method="get" action="/admin_subcategories.php" class="row g-2 align-items-center">
<div class="col-auto flex-grow-1">
<input type="text" name="search" class="form-control" placeholder="Search subcategories..." value="<?= h($search) ?>">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-outline-primary">
<i class="bi bi-search"></i> Search
</button>
<?php if ($search): ?>
<a href="/admin_subcategories.php" class="btn btn-outline-secondary">Clear</a>
<?php endif; ?>
</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="table-light">
<tr>
<th class="ps-4">Name</th>
<th>Parent Category</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($allSubcategories)): ?>
<tr><td colspan="3" class="text-center py-5 text-muted">No subcategories found.</td></tr>
<?php else: ?>
<?php foreach ($allSubcategories as $sub):
$parentName = 'Unknown';
foreach ($categories as $c) {
if ($c['id'] == $sub['category_id']) {
$parentName = $c['name_en'];
break;
}
}
?>
<tr>
<td class="ps-4">
<div class="fw-medium text-dark"><?= h($sub['name_en']) ?></div>
<small class="text-muted"><?= h($sub['name_ar']) ?></small>
</td>
<td>
<span class="badge bg-light text-dark border"><?= h($parentName) ?></span>
</td>
<td class="text-end pe-4">
<button class="btn btn-sm btn-outline-secondary me-1"
data-id="<?= $sub['id'] ?>"
data-category-id="<?= $sub['category_id'] ?>"
data-name-en="<?= h($sub['name_en']) ?>"
data-name-ar="<?= h($sub['name_ar']) ?>"
onclick="openEditSubcategoryModal(this)">
<i class="bi bi-pencil"></i> Edit
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteSubcategory(<?= $sub['id'] ?>)">
<i class="bi bi-trash"></i> Delete
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Subcategory Modal -->
<div class="modal fade" id="subcategoryModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="/admin_subcategories.php" id="subcategoryForm">
<input type="hidden" name="action" id="sub_action" value="create_subcategory">
<input type="hidden" name="id" id="sub_id" value="">
<div class="modal-header">
<h5 class="modal-title" id="subcategoryModalTitle">Add New Subcategory</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Parent Category</label>
<select class="form-select" name="category_id" id="sub_category_id" required>
<option value="">Select...</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>"><?= h($cat['name_en']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Name (English)</label>
<div class="input-group">
<input type="text" class="form-control" name="name_en" id="sub_name_en" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('sub_name_en', 'sub_name_ar', 'Arabic')" title="Translate to Arabic">
<i class="bi bi-translate"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">Name (Arabic)</label>
<div class="input-group">
<input type="text" class="form-control" name="name_ar" id="sub_name_ar" dir="rtl" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('sub_name_ar', 'sub_name_en', 'English')" title="Translate to English">
<i class="bi bi-translate"></i>
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Confirmation Form -->
<form method="post" action="/admin_subcategories.php" id="deleteForm">
<input type="hidden" name="action" id="deleteAction" value="">
<input type="hidden" name="id" id="deleteId" value="">
</form>
<script>
let subcategoryModal;
document.addEventListener('DOMContentLoaded', function() {
subcategoryModal = new bootstrap.Modal(document.getElementById('subcategoryModal'));
});
function openCreateSubcategoryModal() {
document.getElementById('subcategoryModalTitle').innerText = 'Add New Subcategory';
document.getElementById('sub_action').value = 'create_subcategory';
document.getElementById('sub_id').value = '';
document.getElementById('sub_category_id').value = '';
document.getElementById('sub_name_en').value = '';
document.getElementById('sub_name_ar').value = '';
subcategoryModal.show();
}
function openEditSubcategoryModal(btn) {
const id = btn.getAttribute('data-id');
const catId = btn.getAttribute('data-category-id');
const nameEn = btn.getAttribute('data-name-en');
const nameAr = btn.getAttribute('data-name-ar');
document.getElementById('subcategoryModalTitle').innerText = 'Edit Subcategory';
document.getElementById('sub_action').value = 'update_subcategory';
document.getElementById('sub_id').value = id;
document.getElementById('sub_category_id').value = catId;
document.getElementById('sub_name_en').value = nameEn;
document.getElementById('sub_name_ar').value = nameAr;
subcategoryModal.show();
}
function deleteSubcategory(id) {
if (confirm('Are you sure you want to delete this subcategory?')) {
document.getElementById('deleteAction').value = 'delete_subcategory';
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();
}
}
</script>
<?php admin_render_footer(); ?>

212
admin_types.php Normal file
View File

@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/admin_layout.php';
library_bootstrap();
$errors = [];
// Handle POST request
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
try {
if ($action === 'create_type') {
$nameEn = trim($_POST['name_en'] ?? '');
$nameAr = trim($_POST['name_ar'] ?? '');
if (!$nameEn || !$nameAr) {
throw new RuntimeException('Both English and Arabic names are required for Type.');
}
library_create_type($nameEn, $nameAr);
library_set_flash('success', 'Type created successfully.');
header('Location: /admin_types.php');
exit;
} elseif ($action === 'update_type') {
$nameEn = trim($_POST['name_en'] ?? '');
$nameAr = trim($_POST['name_ar'] ?? '');
if (!$id || !$nameEn || !$nameAr) {
throw new RuntimeException('ID, English name, and Arabic name are required.');
}
library_update_type($id, $nameEn, $nameAr);
library_set_flash('success', 'Type updated successfully.');
header('Location: /admin_types.php');
exit;
} elseif ($action === 'delete_type') {
if (!$id) {
throw new RuntimeException('Invalid Type ID.');
}
library_delete_type($id);
library_set_flash('success', 'Type deleted successfully.');
header('Location: /admin_types.php');
exit;
}
} catch (Throwable $exception) {
$errors[] = $exception->getMessage();
}
}
// Search Logic
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$types = library_get_types($search);
admin_render_header('Document Types', 'types');
?>
<!-- Page Content -->
<?php if ($errors): ?>
<div class="alert alert-danger"><?= h(implode(' ', $errors)) ?></div>
<?php endif; ?>
<div class="d-flex justify-content-between align-items-center mb-4">
<p class="text-secondary mb-0">Manage document types (e.g., Document, Film, etc.).</p>
<button class="btn btn-primary" onclick="openCreateTypeModal()">
<i class="bi bi-plus-lg me-1"></i> Add New Type
</button>
</div>
<!-- Search Bar -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<form method="get" action="/admin_types.php" class="row g-2 align-items-center">
<div class="col-auto flex-grow-1">
<input type="text" name="search" class="form-control" placeholder="Search types..." value="<?= h($search) ?>">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-outline-primary">
<i class="bi bi-search"></i> Search
</button>
<?php if ($search): ?>
<a href="/admin_types.php" class="btn btn-outline-secondary">Clear</a>
<?php endif; ?>
</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="table-light">
<tr>
<th class="ps-4">Name</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($types)): ?>
<tr><td colspan="2" class="text-center py-5 text-muted">No types found.</td></tr>
<?php else: ?>
<?php foreach ($types as $type): ?>
<tr>
<td class="ps-4">
<div class="fw-medium text-dark"><?= h($type['name_en']) ?></div>
<small class="text-muted"><?= h($type['name_ar']) ?></small>
</td>
<td class="text-end pe-4">
<button class="btn btn-sm btn-outline-secondary me-1"
data-id="<?= $type['id'] ?>"
data-name-en="<?= h($type['name_en']) ?>"
data-name-ar="<?= h($type['name_ar']) ?>"
onclick="openEditTypeModal(this)">
<i class="bi bi-pencil"></i> Edit
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteType(<?= $type['id'] ?>)">
<i class="bi bi-trash"></i> Delete
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Type Modal -->
<div class="modal fade" id="typeModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="/admin_types.php" id="typeForm">
<input type="hidden" name="action" id="type_action" value="create_type">
<input type="hidden" name="id" id="type_id" value="">
<div class="modal-header">
<h5 class="modal-title" id="typeModalTitle">Add New Type</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Name (English)</label>
<div class="input-group">
<input type="text" class="form-control" name="name_en" id="type_name_en" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('type_name_en', 'type_name_ar', 'Arabic')" title="Translate to Arabic">
<i class="bi bi-translate"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">Name (Arabic)</label>
<div class="input-group">
<input type="text" class="form-control" name="name_ar" id="type_name_ar" dir="rtl" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateText('type_name_ar', 'type_name_en', 'English')" title="Translate to English">
<i class="bi bi-translate"></i>
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Confirmation Form -->
<form method="post" action="/admin_types.php" id="deleteForm">
<input type="hidden" name="action" id="deleteAction" value="">
<input type="hidden" name="id" id="deleteId" value="">
</form>
<script>
let typeModal;
document.addEventListener('DOMContentLoaded', function() {
typeModal = new bootstrap.Modal(document.getElementById('typeModal'));
});
function openCreateTypeModal() {
document.getElementById('typeModalTitle').innerText = 'Add New Type';
document.getElementById('type_action').value = 'create_type';
document.getElementById('type_id').value = '';
document.getElementById('type_name_en').value = '';
document.getElementById('type_name_ar').value = '';
typeModal.show();
}
function openEditTypeModal(btn) {
const id = btn.getAttribute('data-id');
const nameEn = btn.getAttribute('data-name-en');
const nameAr = btn.getAttribute('data-name-ar');
document.getElementById('typeModalTitle').innerText = 'Edit Type';
document.getElementById('type_action').value = 'update_type';
document.getElementById('type_id').value = id;
document.getElementById('type_name_en').value = nameEn;
document.getElementById('type_name_ar').value = nameAr;
typeModal.show();
}
function deleteType(id) {
if (confirm('Are you sure you want to delete this type?')) {
document.getElementById('deleteAction').value = 'delete_type';
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();
}
}
</script>
<?php admin_render_footer(); ?>

View File

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS library_types (
id INT AUTO_INCREMENT PRIMARY KEY,
name_en VARCHAR(255) NOT NULL,
name_ar VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@ -0,0 +1,15 @@
ALTER TABLE library_documents
ADD COLUMN cover_image_path VARCHAR(255) DEFAULT NULL,
ADD COLUMN publisher VARCHAR(255) DEFAULT NULL,
ADD COLUMN publish_year INT DEFAULT NULL,
ADD COLUMN country VARCHAR(100) DEFAULT NULL,
ADD COLUMN type_id INT UNSIGNED DEFAULT NULL,
ADD COLUMN page_count INT DEFAULT NULL,
ADD COLUMN summary_en TEXT DEFAULT NULL,
ADD COLUMN summary_ar TEXT DEFAULT NULL;
-- Add foreign key for type_id if library_types exists (it should from prev migration)
-- We use a safe procedure to add the FK only if the table exists to avoid strict dependency order failures in some setups,
-- but standard SQL is fine here assuming 004 ran.
-- However, strict SQL mode might complain if we don't index it.
ALTER TABLE library_documents ADD INDEX idx_library_type (type_id);

144
includes/admin_layout.php Normal file
View File

@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/library.php';
function admin_render_header(string $title, string $activePage = 'dashboard'): void {
library_bootstrap();
// Get flashes and clear them
$flashes = [];
if (isset($_SESSION['library_flash'])) {
$flashes = $_SESSION['library_flash'];
unset($_SESSION['library_flash']);
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= h($title) ?> · Admin Studio</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
.admin-sidebar {
width: 260px;
height: 100vh;
position: sticky;
top: 0;
background: #fff;
border-right: 1px solid #dee2e6;
padding: 20px;
}
.nav-link {
color: #333;
margin-bottom: 0.25rem;
}
.nav-link:hover {
background-color: #f8f9fa;
}
.nav-link.active {
background-color: #e9ecef;
color: #0d6efd !important;
font-weight: 500;
}
</style>
</head>
<body class="bg-light">
<div class="d-flex">
<aside class="admin-sidebar d-flex flex-column">
<h4 class="mb-4 text-primary fw-bold"><i class="bi bi-grid-fill me-2"></i>Admin Panel</h4>
<nav class="nav flex-column flex-grow-1">
<a class="nav-link rounded <?= $activePage === 'dashboard' ? 'active' : '' ?>" href="/admin.php">
<i class="bi bi-speedometer2 me-2"></i> Dashboard
</a>
<a class="nav-link rounded <?= $activePage === 'documents' ? 'active' : '' ?>" href="/admin_documents.php">
<i class="bi bi-folder2-open me-2"></i> Material Entry
</a>
<div class="my-2 border-top"></div>
<a class="nav-link rounded <?= $activePage === 'categories' ? 'active' : '' ?>" href="/admin_categories.php">
<i class="bi bi-tags me-2"></i> Categories
</a>
<a class="nav-link rounded <?= $activePage === 'subcategories' ? 'active' : '' ?>" href="/admin_subcategories.php">
<i class="bi bi-diagram-3 me-2"></i> Subcategories
</a>
<a class="nav-link rounded <?= $activePage === 'types' ? 'active' : '' ?>" href="/admin_types.php">
<i class="bi bi-file-earmark-text me-2"></i> Types
</a>
</nav>
<div class="mt-auto pt-3 border-top">
<a class="nav-link text-secondary rounded" href="/index.php">
<i class="bi bi-box-arrow-right me-2"></i> Return to Site
</a>
</div>
</aside>
<main class="flex-grow-1 p-4" style="min-width: 0;">
<header class="mb-4 border-bottom pb-2">
<h2 class="h3"><?= h($title) ?></h2>
</header>
<?php foreach ($flashes as $flash): ?>
<div class="alert alert-<?= $flash['type'] === 'error' ? 'danger' : 'success' ?> alert-dismissible fade show shadow-sm" role="alert">
<?= h($flash['message']) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endforeach; ?>
<?php
}
function admin_render_footer(): void {
?>
</main>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Shared Admin JS - Translation Helper
async function translateText(sourceId, targetId, targetLang) {
const source = document.getElementById(sourceId);
const target = document.getElementById(targetId);
const text = source.value.trim();
if (!text) {
alert('Please enter text to translate.');
return;
}
const originalPlaceholder = target.placeholder;
target.placeholder = 'Translating...';
const originalOpacity = target.style.opacity;
target.style.opacity = '0.7';
try {
// Simple mock translation for demo purposes or real API if configured
// In a real scenario, this would call /api/translate.php
// For now, let's assume /api/translate.php exists (it does)
const response = await fetch('/api/translate.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text, target_lang: targetLang })
});
if (!response.ok) throw new Error('Translation failed');
const data = await response.json();
if (data.translation) {
target.value = data.translation;
} else if (data.error) {
alert('Translation error: ' + data.error);
}
} catch (e) {
console.error(e);
alert('Translation failed. Please try again.');
} finally {
target.placeholder = originalPlaceholder;
target.style.opacity = originalOpacity;
}
}
</script>
</body>
</html>
<?php
}
?>

View File

@ -44,10 +44,35 @@ function library_bootstrap(): void
}
}
$migration4Path = __DIR__ . '/../db/migrations/004_create_types_table.sql';
if (is_file($migration4Path)) {
// Simple check if table exists
$exists = db()->query("SHOW TABLES LIKE 'library_types'")->fetch();
if (!$exists) {
$sql = file_get_contents($migration4Path);
db()->exec($sql);
}
}
$migration5Path = __DIR__ . '/../db/migrations/005_update_document_fields.sql';
if (is_file($migration5Path)) {
// Check if column exists
$exists = db()->query("SHOW COLUMNS FROM library_documents LIKE 'cover_image_path'")->fetch();
if (!$exists) {
$sql = file_get_contents($migration5Path);
db()->exec($sql);
}
}
$uploadDir = __DIR__ . '/../uploads/library';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
$coverDir = __DIR__ . '/../uploads/covers';
if (!is_dir($coverDir)) {
mkdir($coverDir, 0775, true);
}
library_seed_demo_documents();
$booted = true;
@ -139,23 +164,44 @@ function library_allowed_extensions(): array
// --- Category Functions ---
function library_get_categories(): array
function library_get_categories(string $search = ''): array
{
library_bootstrap();
$stmt = db()->query('SELECT * FROM library_categories ORDER BY name_en ASC');
return $stmt ? $stmt->fetchAll() : [];
$sql = 'SELECT * FROM library_categories';
$params = [];
if ($search !== '') {
$sql .= ' WHERE name_en LIKE ? OR name_ar LIKE ?';
$params[] = "%$search%";
$params[] = "%$search%";
}
$sql .= ' ORDER BY name_en ASC';
$stmt = db()->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll() ?: [];
}
function library_get_subcategories(?int $categoryId = null): array
function library_get_subcategories(?int $categoryId = null, string $search = ''): array
{
library_bootstrap();
$sql = 'SELECT * FROM library_subcategories WHERE 1=1';
$params = [];
if ($categoryId !== null) {
$stmt = db()->prepare('SELECT * FROM library_subcategories WHERE category_id = ? ORDER BY name_en ASC');
$stmt->execute([$categoryId]);
return $stmt->fetchAll() ?: [];
$sql .= ' AND category_id = ?';
$params[] = $categoryId;
}
$stmt = db()->query('SELECT * FROM library_subcategories ORDER BY name_en ASC');
return $stmt ? $stmt->fetchAll() : [];
if ($search !== '') {
$sql .= ' AND (name_en LIKE ? OR name_ar LIKE ?)';
$params[] = "%$search%";
$params[] = "%$search%";
}
$sql .= ' ORDER BY name_en ASC';
$stmt = db()->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll() ?: [];
}
function library_create_category(string $nameEn, string $nameAr): int
@ -220,14 +266,68 @@ function library_delete_subcategory(int $id): void
// --- End Category Functions ---
// --- Type Functions ---
function library_get_types(string $search = ''): array
{
library_bootstrap();
$sql = 'SELECT * FROM library_types';
$params = [];
if ($search !== '') {
$sql .= ' WHERE name_en LIKE ? OR name_ar LIKE ?';
$params[] = "%$search%";
$params[] = "%$search%";
}
$sql .= ' ORDER BY name_en ASC';
$stmt = db()->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll() ?: [];
}
function library_create_type(string $nameEn, string $nameAr): int
{
library_bootstrap();
$stmt = db()->prepare('INSERT INTO library_types (name_en, name_ar) VALUES (?, ?)');
$stmt->execute([$nameEn, $nameAr]);
return (int) db()->lastInsertId();
}
function library_get_type_by_id(int $id): ?array
{
library_bootstrap();
$stmt = db()->prepare('SELECT * FROM library_types WHERE id = ?');
$stmt->execute([$id]);
return $stmt->fetch() ?: null;
}
function library_update_type(int $id, string $nameEn, string $nameAr): void
{
library_bootstrap();
$stmt = db()->prepare('UPDATE library_types SET name_en = ?, name_ar = ? WHERE id = ?');
$stmt->execute([$nameEn, $nameAr, $id]);
}
function library_delete_type(int $id): void
{
library_bootstrap();
$stmt = db()->prepare('DELETE FROM library_types WHERE id = ?');
$stmt->execute([$id]);
}
// --- End Type Functions ---
function library_fetch_documents(bool $publicOnly = false, array $filters = []): array
{
library_bootstrap();
$sql = 'SELECT d.*, c.name_en as cat_en, c.name_ar as cat_ar, sc.name_en as sub_en, sc.name_ar as sub_ar
$sql = 'SELECT d.*,
c.name_en as cat_en, c.name_ar as cat_ar,
sc.name_en as sub_en, sc.name_ar as sub_ar,
t.name_en as type_en, t.name_ar as type_ar
FROM library_documents d
LEFT JOIN library_categories c ON d.category_id = c.id
LEFT JOIN library_subcategories sc ON d.subcategory_id = sc.id
LEFT JOIN library_types t ON d.type_id = t.id
WHERE 1=1';
$params = [];
@ -246,6 +346,35 @@ function library_fetch_documents(bool $publicOnly = false, array $filters = []):
return $stmt->fetchAll() ?: [];
}
function library_fetch_document(int $id, bool $publicOnly = false): ?array
{
library_bootstrap();
$sql = 'SELECT d.*,
c.name_en as cat_en, c.name_ar as cat_ar,
sc.name_en as sub_en, sc.name_ar as sub_ar,
t.name_en as type_en, t.name_ar as type_ar
FROM library_documents d
LEFT JOIN library_categories c ON d.category_id = c.id
LEFT JOIN library_subcategories sc ON d.subcategory_id = sc.id
LEFT JOIN library_types t ON d.type_id = t.id
WHERE d.id = :id';
$params = [':id' => $id];
if ($publicOnly) {
$sql .= ' AND d.visibility = :visibility';
$params[':visibility'] = 'public';
}
$stmt = db()->prepare($sql);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->execute();
return $stmt->fetch() ?: null;
}
function library_recent_documents(int $limit = 3, bool $publicOnly = false): array
{
library_bootstrap();
@ -316,7 +445,37 @@ function library_handle_uploaded_file(array $file): array
];
}
function library_create_document(array $payload, array $file): int
function library_handle_cover_image(array $file): ?string
{
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
return null;
}
$originalName = (string) ($file['name'] ?? '');
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
$allowed = ['jpg', 'jpeg', 'png', 'webp', 'gif'];
if (!in_array($extension, $allowed)) {
throw new RuntimeException('Unsupported cover image type. Allowed: jpg, png, webp, gif.');
}
$size = (int) ($file['size'] ?? 0);
if ($size <= 0 || $size > 5 * 1024 * 1024) {
throw new RuntimeException('Cover image must be smaller than 5 MB.');
}
$safeBase = preg_replace('/[^a-zA-Z0-9_-]+/', '-', pathinfo($originalName, PATHINFO_FILENAME)) ?: 'cover';
$storedName = strtolower(date('YmdHis') . '-' . $safeBase . '-' . bin2hex(random_bytes(4)) . '.' . $extension);
$relativePath = 'uploads/covers/' . $storedName;
$absolutePath = __DIR__ . '/../' . $relativePath;
if (!move_uploaded_file((string) $file['tmp_name'], $absolutePath)) {
throw new RuntimeException('Unable to save the cover image.');
}
return $relativePath;
}
function library_create_document(array $payload, array $file, array $coverFile = []): int
{
library_bootstrap();
@ -326,6 +485,7 @@ function library_create_document(array $payload, array $file): int
// Process IDs
$categoryId = !empty($payload['category_id']) ? (int)$payload['category_id'] : null;
$subcategoryId = !empty($payload['subcategory_id']) ? (int)$payload['subcategory_id'] : null;
$typeId = !empty($payload['type_id']) ? (int)$payload['type_id'] : null;
// Fetch names for backward compatibility if needed, or just store IDs
$categoryName = '';
@ -353,20 +513,32 @@ function library_create_document(array $payload, array $file): int
$allow_print = !empty($payload['allow_print']) ? 1 : 0;
$allow_copy = !empty($payload['allow_copy']) ? 1 : 0;
// New Fields
$publisher = trim((string) ($payload['publisher'] ?? ''));
$publishYear = !empty($payload['publish_year']) ? (int)$payload['publish_year'] : null;
$author = trim((string) ($payload['author'] ?? ''));
$country = trim((string) ($payload['country'] ?? ''));
$pageCount = !empty($payload['page_count']) ? (int)$payload['page_count'] : null;
$summaryEn = trim((string) ($payload['summary_en'] ?? ''));
$summaryAr = trim((string) ($payload['summary_ar'] ?? ''));
$fileData = library_handle_uploaded_file($file);
$coverPath = library_handle_cover_image($coverFile);
$stmt = db()->prepare('INSERT INTO library_documents (
title_en, title_ar,
category, category_ar, sub_category, sub_category_ar,
category_id, subcategory_id,
visibility, document_type,
file_name, file_path, file_size_kb, allow_download, allow_print, allow_copy
file_name, file_path, file_size_kb, allow_download, allow_print, allow_copy,
cover_image_path, publisher, publish_year, author, country, type_id, page_count, summary_en, summary_ar
) VALUES (
:title_en, :title_ar,
:category, :category_ar, :sub_category, :sub_category_ar,
:category_id, :subcategory_id,
:visibility, :document_type,
:file_name, :file_path, :file_size_kb, :allow_download, :allow_print, :allow_copy
:file_name, :file_path, :file_size_kb, :allow_download, :allow_print, :allow_copy,
:cover_image_path, :publisher, :publish_year, :author, :country, :type_id, :page_count, :summary_en, :summary_ar
)');
$stmt->execute([
@ -386,7 +558,140 @@ function library_create_document(array $payload, array $file): int
':allow_download' => $allow_download,
':allow_print' => $allow_print,
':allow_copy' => $allow_copy,
':cover_image_path' => $coverPath,
':publisher' => $publisher ?: null,
':publish_year' => $publishYear,
':author' => $author ?: null,
':country' => $country ?: null,
':type_id' => $typeId,
':page_count' => $pageCount,
':summary_en' => $summaryEn ?: null,
':summary_ar' => $summaryAr ?: null,
]);
return (int) db()->lastInsertId();
}
function library_update_document(int $id, array $payload, array $file = [], array $coverFile = []): void
{
library_bootstrap();
// Fetch existing document to keep file if not replaced
$existing = library_fetch_document($id);
if (!$existing) {
throw new RuntimeException('Document not found.');
}
$titleEn = trim((string) ($payload['title_en'] ?? ''));
$titleAr = trim((string) ($payload['title_ar'] ?? ''));
// Process IDs
$categoryId = !empty($payload['category_id']) ? (int)$payload['category_id'] : null;
$subcategoryId = !empty($payload['subcategory_id']) ? (int)$payload['subcategory_id'] : null;
$typeId = !empty($payload['type_id']) ? (int)$payload['type_id'] : null;
$categoryName = '';
$categoryNameAr = '';
$subName = '';
$subNameAr = '';
if ($categoryId) {
$cat = library_get_category_by_id($categoryId);
if ($cat) {
$categoryName = $cat['name_en'];
$categoryNameAr = $cat['name_ar'];
}
}
if ($subcategoryId) {
$sub = library_get_subcategory_by_id($subcategoryId);
if ($sub) {
$subName = $sub['name_en'];
$subNameAr = $sub['name_ar'];
}
}
$visibility = (string) ($payload['visibility'] ?? 'public');
$allow_download = !empty($payload['allow_download']) ? 1 : 0;
$allow_print = !empty($payload['allow_print']) ? 1 : 0;
$allow_copy = !empty($payload['allow_copy']) ? 1 : 0;
$publisher = trim((string) ($payload['publisher'] ?? ''));
$publishYear = !empty($payload['publish_year']) ? (int)$payload['publish_year'] : null;
$author = trim((string) ($payload['author'] ?? ''));
$country = trim((string) ($payload['country'] ?? ''));
$pageCount = !empty($payload['page_count']) ? (int)$payload['page_count'] : null;
$summaryEn = trim((string) ($payload['summary_en'] ?? ''));
$summaryAr = trim((string) ($payload['summary_ar'] ?? ''));
// Handle File Update
$fileData = null;
if (!empty($file['name'])) {
$fileData = library_handle_uploaded_file($file);
}
// Handle Cover Update
$coverPath = null;
if (!empty($coverFile['name'])) {
$coverPath = library_handle_cover_image($coverFile);
}
$sql = 'UPDATE library_documents SET
title_en = :title_en, title_ar = :title_ar,
category = :category, category_ar = :category_ar, sub_category = :sub_category, sub_category_ar = :sub_category_ar,
category_id = :category_id, subcategory_id = :subcategory_id,
visibility = :visibility,
allow_download = :allow_download, allow_print = :allow_print, allow_copy = :allow_copy,
publisher = :publisher, publish_year = :publish_year, author = :author, country = :country,
type_id = :type_id, page_count = :page_count, summary_en = :summary_en, summary_ar = :summary_ar';
$params = [
':title_en' => $titleEn ?: null,
':title_ar' => $titleAr ?: null,
':category' => $categoryName ?: null,
':category_ar' => $categoryNameAr ?: null,
':sub_category' => $subName ?: null,
':sub_category_ar' => $subNameAr ?: null,
':category_id' => $categoryId,
':subcategory_id' => $subcategoryId,
':visibility' => $visibility,
':allow_download' => $allow_download,
':allow_print' => $allow_print,
':allow_copy' => $allow_copy,
':publisher' => $publisher ?: null,
':publish_year' => $publishYear,
':author' => $author ?: null,
':country' => $country ?: null,
':type_id' => $typeId,
':page_count' => $pageCount,
':summary_en' => $summaryEn ?: null,
':summary_ar' => $summaryAr ?: null,
':id' => $id,
];
if ($fileData) {
$sql .= ', document_type = :document_type, file_name = :file_name, file_path = :file_path, file_size_kb = :file_size_kb';
$params[':document_type'] = $fileData['document_type'];
$params[':file_name'] = $fileData['file_name'];
$params[':file_path'] = $fileData['file_path'];
$params[':file_size_kb'] = $fileData['file_size_kb'];
}
if ($coverPath) {
$sql .= ', cover_image_path = :cover_image_path';
$params[':cover_image_path'] = $coverPath;
}
$sql .= ' WHERE id = :id';
$stmt = db()->prepare($sql);
$stmt->execute($params);
}
function library_delete_document(int $id): void
{
library_bootstrap();
// Optionally delete files, but for safety we might keep them or delete them.
// For now, just delete DB record.
$stmt = db()->prepare('DELETE FROM library_documents WHERE id = ?');
$stmt->execute([$id]);
}