-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ Manage your library documents, upload new materials, and edit metadata including bilingual titles, summaries, and more.
+
+
+ Manage Documents
+
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/admin_categories.php b/admin_categories.php
new file mode 100644
index 0000000..13ff438
--- /dev/null
+++ b/admin_categories.php
@@ -0,0 +1,212 @@
+getMessage();
+ }
+}
+
+// Search Logic
+$search = isset($_GET['search']) ? trim($_GET['search']) : '';
+$categories = library_get_categories($search);
+
+admin_render_header('Categories', 'categories');
+?>
+
+
+
= h(implode(' ', $errors)) ?>
+
+
+
+
Manage document categories.
+
+
+
+
+
+
+
+
+
+
+
+
+ | Name |
+ Actions |
+
+
+
+
+ | No categories found. |
+
+
+
+ |
+ = h($cat['name_en']) ?>
+ = h($cat['name_ar']) ?>
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin_documents.php b/admin_documents.php
new file mode 100644
index 0000000..d8adae1
--- /dev/null
+++ b/admin_documents.php
@@ -0,0 +1,423 @@
+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');
+?>
+
+
+
= h(implode(' ', $errors)) ?>
+
+
+
+
Manage library documents (Material Entry).
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Cover |
+ Title / Author |
+ Type / Category |
+ Year |
+ Actions |
+
+
+
+
+ | No documents found. |
+
+
+
+ | #= $doc['id'] ?> |
+
+
+
+
+
+
+
+
+ |
+
+ = h($doc['title_en']) ?>
+ = h($doc['title_ar']) ?>
+
+ = h($doc['author']) ?>
+
+ |
+
+
+ = h($doc['type_en']) ?>
+
+ = h($doc['cat_en'] ?? $doc['category']) ?>
+
+
+ = h($doc['sub_en']) ?>
+
+ |
+ = h((string)$doc['publish_year']) ?> |
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin_subcategories.php b/admin_subcategories.php
new file mode 100644
index 0000000..3b5a776
--- /dev/null
+++ b/admin_subcategories.php
@@ -0,0 +1,240 @@
+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');
+?>
+
+
+
= h(implode(' ', $errors)) ?>
+
+
+
+
Manage document subcategories.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Name |
+ Parent Category |
+ Actions |
+
+
+
+
+ | No subcategories found. |
+
+
+
+ |
+ = h($sub['name_en']) ?>
+ = h($sub['name_ar']) ?>
+ |
+
+ = h($parentName) ?>
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin_types.php b/admin_types.php
new file mode 100644
index 0000000..3db91e3
--- /dev/null
+++ b/admin_types.php
@@ -0,0 +1,212 @@
+getMessage();
+ }
+}
+
+// Search Logic
+$search = isset($_GET['search']) ? trim($_GET['search']) : '';
+$types = library_get_types($search);
+
+admin_render_header('Document Types', 'types');
+?>
+
+
+
= h(implode(' ', $errors)) ?>
+
+
+
+
Manage document types (e.g., Document, Film, etc.).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Name |
+ Actions |
+
+
+
+
+ | No types found. |
+
+
+
+ |
+ = h($type['name_en']) ?>
+ = h($type['name_ar']) ?>
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/db/migrations/004_create_types_table.sql b/db/migrations/004_create_types_table.sql
new file mode 100644
index 0000000..681728b
--- /dev/null
+++ b/db/migrations/004_create_types_table.sql
@@ -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;
diff --git a/db/migrations/005_update_document_fields.sql b/db/migrations/005_update_document_fields.sql
new file mode 100644
index 0000000..fcd62e9
--- /dev/null
+++ b/db/migrations/005_update_document_fields.sql
@@ -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);
diff --git a/includes/admin_layout.php b/includes/admin_layout.php
new file mode 100644
index 0000000..e0f04fe
--- /dev/null
+++ b/includes/admin_layout.php
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
= h($title) ?> · Admin Studio
+
+
+
+
+
+
+
+
+
+
+
+
+ = h($flash['message']) ?>
+
+
+
+
+
+
+
+
+
+
+
diff --git a/includes/library.php b/includes/library.php
index f1fbd90..58a02bd 100644
--- a/includes/library.php
+++ b/includes/library.php
@@ -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]);
}
\ No newline at end of file