update display

This commit is contained in:
Flatlogic Bot 2026-04-15 07:14:44 +00:00
parent 3ec25535e5
commit bfd0f1a7e8
2 changed files with 365 additions and 154 deletions

View File

@ -4,17 +4,45 @@ require_once __DIR__ . '/queue_bootstrap.php';
qh_boot(); qh_boot();
qh_admin_handle_request(); qh_admin_handle_request();
// Ensure the table exists // Ensure the tables exist
$pdo = db(); $pdo = db();
function qh_ads_column_exists(PDO $pdo, string $column): bool
{
static $cache = [];
if (array_key_exists($column, $cache)) {
return $cache[$column];
}
$stmt = $pdo->prepare(
"SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'hospital_ads' AND COLUMN_NAME = ?"
);
$stmt->execute([DB_NAME, $column]);
return $cache[$column] = ((int) $stmt->fetchColumn() > 0);
}
$pdo->exec("CREATE TABLE IF NOT EXISTS hospital_ads ( $pdo->exec("CREATE TABLE IF NOT EXISTS hospital_ads (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NULL, title VARCHAR(255) NULL,
video_path VARCHAR(255) NOT NULL, video_path VARCHAR(255) NOT NULL,
media_type VARCHAR(20) NOT NULL DEFAULT 'video',
media_path VARCHAR(255) NULL,
is_active TINYINT(1) DEFAULT 1, is_active TINYINT(1) DEFAULT 1,
sort_order INT DEFAULT 0, sort_order INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)"); )");
if (!qh_ads_column_exists($pdo, 'media_type')) {
$pdo->exec("ALTER TABLE hospital_ads ADD COLUMN media_type VARCHAR(20) NOT NULL DEFAULT 'video' AFTER video_path");
}
if (!qh_ads_column_exists($pdo, 'media_path')) {
$pdo->exec("ALTER TABLE hospital_ads ADD COLUMN media_path VARCHAR(255) NULL AFTER media_type");
}
$pdo->exec("UPDATE hospital_ads SET media_type = 'video' WHERE media_type IS NULL OR media_type = ''");
$pdo->exec("UPDATE hospital_ads SET media_path = video_path WHERE (media_path IS NULL OR media_path = '') AND video_path <> ''");
$pdo->exec("CREATE TABLE IF NOT EXISTS hospital_news ( $pdo->exec("CREATE TABLE IF NOT EXISTS hospital_news (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
phrase VARCHAR(1000) NOT NULL, phrase VARCHAR(1000) NOT NULL,
@ -26,18 +54,30 @@ $pdo->exec("CREATE TABLE IF NOT EXISTS hospital_news (
// Handle Form Submissions // Handle Form Submissions
$message = ''; $message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['action']) && $_POST['action'] === 'add_video' && !empty($_FILES['video']['name'])) { $action = (string) ($_POST['action'] ?? '');
$uploadDir = __DIR__ . '/assets/videos/uploads/'; $adId = (int) ($_POST['ad_id'] ?? $_POST['video_id'] ?? 0);
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
$ext = strtolower(pathinfo($_FILES['video']['name'], PATHINFO_EXTENSION)); if ($action === 'add_media' && !empty($_FILES['media']['name'])) {
$allowed = ['mp4', 'webm', 'ogg']; $mediaFile = $_FILES['media'];
$title = trim((string) ($_POST['title'] ?? ''));
$ext = strtolower(pathinfo($mediaFile['name'], PATHINFO_EXTENSION));
$typeMap = [
'mp4' => 'video',
'webm' => 'video',
'ogg' => 'video',
'jpg' => 'image',
'jpeg' => 'image',
'png' => 'image',
'webp' => 'image',
'gif' => 'image',
];
if (!in_array($ext, $allowed)) { if (!isset($typeMap[$ext])) {
$message = qh_t("Invalid video format (.$ext). Allowed formats: MP4, WebM, OGG.", "تنسيق فيديو غير صالح (.$ext). الصيغ المسموحة: MP4, WebM, OGG."); $message = qh_t(
} elseif ($_FILES['video']['error'] !== UPLOAD_ERR_OK) { "Invalid media format (.$ext). Allowed formats: JPG, JPEG, PNG, WEBP, GIF, MP4, WebM, OGG.",
"تنسيق وسائط غير صالح (.$ext). الصيغ المسموحة: JPG, JPEG, PNG, WEBP, GIF, MP4, WebM, OGG."
);
} elseif ($mediaFile['error'] !== UPLOAD_ERR_OK) {
$errorMap = [ $errorMap = [
UPLOAD_ERR_INI_SIZE => 'File exceeds max size in php.ini.', UPLOAD_ERR_INI_SIZE => 'File exceeds max size in php.ini.',
UPLOAD_ERR_FORM_SIZE => 'File exceeds max size in HTML form.', UPLOAD_ERR_FORM_SIZE => 'File exceeds max size in HTML form.',
@ -45,53 +85,66 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
UPLOAD_ERR_NO_FILE => 'No file was uploaded.', UPLOAD_ERR_NO_FILE => 'No file was uploaded.',
UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder.', UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder.',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.', UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.',
UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the upload.' UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the upload.',
]; ];
$errMsg = $errorMap[$_FILES['video']['error']] ?? 'Unknown error'; $errMsg = $errorMap[$mediaFile['error']] ?? 'Unknown error';
$message = qh_t('Upload error: ' . $errMsg, 'خطأ في الرفع: ' . $errMsg); $message = qh_t('Upload error: ' . $errMsg, 'خطأ في الرفع: ' . $errMsg);
} else { } else {
$filename = uniqid('vid_') . '.' . $ext; $mediaType = $typeMap[$ext];
$uploadDir = __DIR__ . ($mediaType === 'video' ? '/assets/videos/uploads/' : '/assets/images/uploads/');
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
$filename = uniqid($mediaType === 'video' ? 'vid_' : 'img_') . '.' . $ext;
$dest = $uploadDir . $filename; $dest = $uploadDir . $filename;
if (move_uploaded_file($_FILES['video']['tmp_name'], $dest)) { if (move_uploaded_file($mediaFile['tmp_name'], $dest)) {
$title = $_POST['title'] ?? ''; $path = ($mediaType === 'video' ? 'assets/videos/uploads/' : 'assets/images/uploads/') . $filename;
$path = 'assets/videos/uploads/' . $filename; $legacyVideoPath = $mediaType === 'video' ? $path : '';
$stmt = $pdo->prepare("INSERT INTO hospital_ads (title, video_path, is_active) VALUES (?, ?, 1)"); $stmt = $pdo->prepare(
$stmt->execute([$title, $path]); "INSERT INTO hospital_ads (title, video_path, media_type, media_path, is_active) VALUES (?, ?, ?, ?, 1)"
$message = qh_t('Video uploaded successfully.', 'تم رفع الفيديو بنجاح.'); );
$stmt->execute([$title, $legacyVideoPath, $mediaType, $path]);
$message = $mediaType === 'video'
? qh_t('Video uploaded successfully.', 'تم رفع الفيديو بنجاح.')
: qh_t('Image uploaded successfully.', 'تم رفع الصورة بنجاح.');
} else { } else {
$message = qh_t('Failed to save uploaded file.', 'فشل في حفظ الملف المرفوع.'); $message = qh_t('Failed to save uploaded file.', 'فشل في حفظ الملف المرفوع.');
} }
} }
} elseif (isset($_POST['action']) && $_POST['action'] === 'delete_video' && !empty($_POST['video_id'])) { } elseif ($action === 'delete_media' && $adId > 0) {
$id = (int) $_POST['video_id']; $stmt = $pdo->prepare(
$stmt = $pdo->prepare("SELECT video_path FROM hospital_ads WHERE id = ?"); "SELECT COALESCE(NULLIF(media_path, ''), NULLIF(video_path, '')) AS file_path FROM hospital_ads WHERE id = ?"
$stmt->execute([$id]); );
$video = $stmt->fetch(); $stmt->execute([$adId]);
if ($video) { $media = $stmt->fetch();
$fullPath = __DIR__ . '/' . $video['video_path']; if ($media) {
if (file_exists($fullPath)) { $filePath = (string) ($media['file_path'] ?? '');
unlink($fullPath); if ($filePath !== '') {
$fullPath = __DIR__ . '/' . $filePath;
if (file_exists($fullPath)) {
unlink($fullPath);
}
} }
$pdo->prepare("DELETE FROM hospital_ads WHERE id = ?")->execute([$id]); $pdo->prepare("DELETE FROM hospital_ads WHERE id = ?")->execute([$adId]);
$message = qh_t('Video deleted successfully.', 'تم حذف الفيديو بنجاح.'); $message = qh_t('Ad deleted successfully.', 'تم حذف الإعلان بنجاح.');
} }
} elseif (isset($_POST['action']) && $_POST['action'] === 'toggle_status' && !empty($_POST['video_id'])) { } elseif ($action === 'toggle_status' && $adId > 0) {
$id = (int) $_POST['video_id'];
$stmt = $pdo->prepare("UPDATE hospital_ads SET is_active = NOT is_active WHERE id = ?"); $stmt = $pdo->prepare("UPDATE hospital_ads SET is_active = NOT is_active WHERE id = ?");
$stmt->execute([$id]); $stmt->execute([$adId]);
$message = qh_t('Video status updated.', 'تم تحديث حالة الفيديو.'); $message = qh_t('Ad status updated.', 'تم تحديث حالة الإعلان.');
} elseif (isset($_POST['action']) && $_POST['action'] === 'add_news' && !empty($_POST['phrase'])) { } elseif ($action === 'add_news' && !empty($_POST['phrase'])) {
$phrase = trim($_POST['phrase']); $phrase = trim($_POST['phrase']);
$stmt = $pdo->prepare("INSERT INTO hospital_news (phrase, is_active) VALUES (?, 1)"); $stmt = $pdo->prepare("INSERT INTO hospital_news (phrase, is_active) VALUES (?, 1)");
$stmt->execute([$phrase]); $stmt->execute([$phrase]);
$message = qh_t('Phrase added successfully.', 'تمت إضافة العبارة بنجاح.'); $message = qh_t('Phrase added successfully.', 'تمت إضافة العبارة بنجاح.');
} elseif (isset($_POST['action']) && $_POST['action'] === 'delete_news' && !empty($_POST['news_id'])) { } elseif ($action === 'delete_news' && !empty($_POST['news_id'])) {
$id = (int) $_POST['news_id']; $id = (int) $_POST['news_id'];
$pdo->prepare("DELETE FROM hospital_news WHERE id = ?")->execute([$id]); $pdo->prepare("DELETE FROM hospital_news WHERE id = ?")->execute([$id]);
$message = qh_t('Phrase deleted successfully.', 'تم حذف العبارة بنجاح.'); $message = qh_t('Phrase deleted successfully.', 'تم حذف العبارة بنجاح.');
} elseif (isset($_POST['action']) && $_POST['action'] === 'toggle_news_status' && !empty($_POST['news_id'])) { } elseif ($action === 'toggle_news_status' && !empty($_POST['news_id'])) {
$id = (int) $_POST['news_id']; $id = (int) $_POST['news_id'];
$pdo->prepare("UPDATE hospital_news SET is_active = NOT is_active WHERE id = ?")->execute([$id]); $pdo->prepare("UPDATE hospital_news SET is_active = NOT is_active WHERE id = ?")->execute([$id]);
$message = qh_t('Phrase status updated.', 'تم تحديث حالة العبارة.'); $message = qh_t('Phrase status updated.', 'تم تحديث حالة العبارة.');
@ -99,7 +152,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
// Fetch existing ads // Fetch existing ads
$stmt = $pdo->query("SELECT * FROM hospital_ads ORDER BY sort_order ASC, id DESC"); $stmt = $pdo->query(
"SELECT *,
COALESCE(NULLIF(media_type, ''), 'video') AS effective_media_type,
COALESCE(NULLIF(media_path, ''), NULLIF(video_path, '')) AS effective_media_path
FROM hospital_ads
ORDER BY sort_order ASC, id DESC"
);
$ads = $stmt->fetchAll(); $ads = $stmt->fetchAll();
$stmt2 = $pdo->query("SELECT * FROM hospital_news ORDER BY sort_order ASC, id DESC"); $stmt2 = $pdo->query("SELECT * FROM hospital_news ORDER BY sort_order ASC, id DESC");
@ -108,7 +167,7 @@ $newsPhrases = $stmt2->fetchAll();
qh_page_start( qh_page_start(
'admin_ads', 'admin_ads',
qh_t('Manage Advertisements', 'إدارة الإعلانات'), qh_t('Manage Advertisements', 'إدارة الإعلانات'),
qh_t('Upload and manage videos to display on the queue screen.', 'رفع وإدارة الفيديوهات لعرضها على شاشة الطابور.') qh_t('Upload and manage image and video ads for the queue display.', 'رفع وإدارة إعلانات الصور والفيديوهات لعرضها على شاشة الطابور.')
); );
?> ?>
<div class="container-fluid container-xxl px-3 px-lg-4"> <div class="container-fluid container-xxl px-3 px-lg-4">
@ -120,8 +179,8 @@ qh_page_start(
<div class="admin-content-stack"> <div class="admin-content-stack">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<div> <div>
<h1 class="h3 mb-0 text-gray-800 fw-bold"><?= qh_h(qh_t('Advertisements (Videos)', 'الإعلانات (فيديوهات)')) ?></h1> <h1 class="h3 mb-0 text-gray-800 fw-bold"><?= qh_h(qh_t('Advertisements (Images & Videos)', 'الإعلانات (صور وفيديوهات)')) ?></h1>
<p class="text-muted mb-0 mt-1"><?= qh_h(qh_t('These videos will be played sequentially on the display screen.', 'سيتم تشغيل هذه الفيديوهات بالتتابع على شاشة العرض.')) ?></p> <p class="text-muted mb-0 mt-1"><?= qh_h(qh_t('Videos play until they end, while images stay on screen for 10 seconds in the same rotation.', 'تعمل الفيديوهات حتى نهايتها، بينما تبقى الصور على الشاشة لمدة 10 ثوانٍ ضمن نفس التدوير.')) ?></p>
</div> </div>
</div> </div>
@ -131,22 +190,22 @@ qh_page_start(
<div class="card shadow-sm border-0 mb-4"> <div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom py-3"> <div class="card-header bg-white border-bottom py-3">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Upload New Video', 'رفع فيديو جديد')) ?></h5> <h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Upload New Ad Media', 'رفع وسائط إعلان جديدة')) ?></h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="post" enctype="multipart/form-data" class="row g-3"> <form method="post" enctype="multipart/form-data" class="row g-3">
<input type="hidden" name="action" value="add_video"> <input type="hidden" name="action" value="add_media">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-semibold text-dark"><?= qh_h(qh_t('Video Title (Optional)', 'عنوان الفيديو (اختياري)')) ?></label> <label class="form-label fw-semibold text-dark"><?= qh_h(qh_t('Ad Title (Optional)', 'عنوان الإعلان (اختياري)')) ?></label>
<input type="text" name="title" class="form-control" placeholder="<?= qh_h(qh_t('e.g. Summer Promo', 'مثال: عرض الصيف')) ?>"> <input type="text" name="title" class="form-control" placeholder="<?= qh_h(qh_t('e.g. Summer Promo', 'مثال: عرض الصيف')) ?>">
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label fw-semibold text-dark"><?= qh_h(qh_t('Video File', 'ملف الفيديو')) ?></label> <label class="form-label fw-semibold text-dark"><?= qh_h(qh_t('Image or Video File', 'ملف صورة أو فيديو')) ?></label>
<input type="file" name="video" class="form-control" accept="video/mp4,video/webm,video/ogg" required> <input type="file" name="media" class="form-control" accept="image/jpeg,image/png,image/webp,image/gif,video/mp4,video/webm,video/ogg" required>
<div class="form-text"><?= qh_h(qh_t('Formats: MP4, WebM, OGG.', 'الصيغ: MP4, WebM, OGG.')) ?></div> <div class="form-text"><?= qh_h(qh_t('Formats: JPG, JPEG, PNG, WEBP, GIF, MP4, WebM, OGG.', 'الصيغ: JPG, JPEG, PNG, WEBP, GIF, MP4, WebM, OGG.')) ?></div>
</div> </div>
<div class="col-12 mt-3"> <div class="col-12 mt-3">
<button type="submit" class="btn btn-primary px-4"><?= qh_h(qh_t('Upload Video', 'رفع الفيديو')) ?></button> <button type="submit" class="btn btn-primary px-4"><?= qh_h(qh_t('Upload Ad', 'رفع الإعلان')) ?></button>
</div> </div>
</form> </form>
</div> </div>
@ -154,7 +213,7 @@ qh_page_start(
<div class="card shadow-sm border-0"> <div class="card shadow-sm border-0">
<div class="card-header bg-white border-bottom py-3"> <div class="card-header bg-white border-bottom py-3">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Uploaded Videos', 'الفيديوهات المرفوعة')) ?></h5> <h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Uploaded Ads', 'الإعلانات المرفوعة')) ?></h5>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
<div class="table-responsive"> <div class="table-responsive">
@ -163,6 +222,7 @@ qh_page_start(
<tr> <tr>
<th class="border-0 px-4 py-3"><?= qh_h(qh_t('Preview', 'معاينة')) ?></th> <th class="border-0 px-4 py-3"><?= qh_h(qh_t('Preview', 'معاينة')) ?></th>
<th class="border-0 py-3"><?= qh_h(qh_t('Title', 'العنوان')) ?></th> <th class="border-0 py-3"><?= qh_h(qh_t('Title', 'العنوان')) ?></th>
<th class="border-0 py-3 text-center"><?= qh_h(qh_t('Type', 'النوع')) ?></th>
<th class="border-0 py-3 text-center"><?= qh_h(qh_t('Status', 'الحالة')) ?></th> <th class="border-0 py-3 text-center"><?= qh_h(qh_t('Status', 'الحالة')) ?></th>
<th class="border-0 px-4 py-3 text-end"><?= qh_h(qh_t('Actions', 'الإجراءات')) ?></th> <th class="border-0 px-4 py-3 text-end"><?= qh_h(qh_t('Actions', 'الإجراءات')) ?></th>
</tr> </tr>
@ -170,19 +230,28 @@ qh_page_start(
<tbody> <tbody>
<?php if (empty($ads)): ?> <?php if (empty($ads)): ?>
<tr> <tr>
<td colspan="4" class="text-center py-4 text-muted"><?= qh_h(qh_t('No videos uploaded yet.', 'لم يتم رفع فيديوهات بعد.')) ?></td> <td colspan="5" class="text-center py-4 text-muted"><?= qh_h(qh_t('No ads uploaded yet.', 'لم يتم رفع أي إعلانات بعد.')) ?></td>
</tr> </tr>
<?php else: ?> <?php else: ?>
<?php foreach ($ads as $ad): ?> <?php foreach ($ads as $ad): ?>
<tr> <tr>
<td class="px-4 py-3"> <td class="px-4 py-3">
<video width="120" class="rounded shadow-sm border" muted> <?php if (($ad['effective_media_type'] ?? 'video') === 'image'): ?>
<source src="<?= qh_h($ad['video_path']) ?>" type="video/mp4"> <img src="<?= qh_h((string) $ad['effective_media_path']) ?>" alt="<?= qh_h($ad['title'] ?: qh_t('Advertisement preview', 'معاينة الإعلان')) ?>" width="120" height="68" class="rounded shadow-sm border object-fit-cover">
</video> <?php else: ?>
<video width="120" class="rounded shadow-sm border" muted playsinline>
<source src="<?= qh_h((string) $ad['effective_media_path']) ?>" type="video/mp4">
</video>
<?php endif; ?>
</td> </td>
<td class="py-3 fw-semibold text-dark"> <td class="py-3 fw-semibold text-dark">
<?= qh_h($ad['title'] ?: qh_t('Untitled', 'بدون عنوان')) ?> <?= qh_h($ad['title'] ?: qh_t('Untitled', 'بدون عنوان')) ?>
</td> </td>
<td class="py-3 text-center">
<span class="badge <?= ($ad['effective_media_type'] ?? 'video') === 'image' ? 'bg-info-subtle text-info-emphasis' : 'bg-dark-subtle text-dark-emphasis' ?> rounded-pill px-3">
<?= qh_h(($ad['effective_media_type'] ?? 'video') === 'image' ? qh_t('Image', 'صورة') : qh_t('Video', 'فيديو')) ?>
</span>
</td>
<td class="py-3 text-center"> <td class="py-3 text-center">
<?php if ($ad['is_active']): ?> <?php if ($ad['is_active']): ?>
<span class="badge bg-success rounded-pill px-3"><?= qh_h(qh_t('Active', 'مفعل')) ?></span> <span class="badge bg-success rounded-pill px-3"><?= qh_h(qh_t('Active', 'مفعل')) ?></span>
@ -194,14 +263,14 @@ qh_page_start(
<div class="d-flex justify-content-end gap-2"> <div class="d-flex justify-content-end gap-2">
<form method="post" class="m-0 p-0"> <form method="post" class="m-0 p-0">
<input type="hidden" name="action" value="toggle_status"> <input type="hidden" name="action" value="toggle_status">
<input type="hidden" name="video_id" value="<?= (int) $ad['id'] ?>"> <input type="hidden" name="ad_id" value="<?= (int) $ad['id'] ?>">
<button type="submit" class="btn btn-sm <?= $ad['is_active'] ? 'btn-outline-secondary' : 'btn-outline-success' ?>"> <button type="submit" class="btn btn-sm <?= $ad['is_active'] ? 'btn-outline-secondary' : 'btn-outline-success' ?>">
<?= qh_h($ad['is_active'] ? qh_t('Disable', 'تعطيل') : qh_t('Enable', 'تفعيل')) ?> <?= qh_h($ad['is_active'] ? qh_t('Disable', 'تعطيل') : qh_t('Enable', 'تفعيل')) ?>
</button> </button>
</form> </form>
<form method="post" class="m-0 p-0" onsubmit="return confirm('<?= qh_h(qh_t('Are you sure you want to delete this video?', 'هل أنت متأكد أنك تريد حذف هذا الفيديو؟')) ?>');"> <form method="post" class="m-0 p-0" onsubmit="return confirm('<?= qh_h(qh_t('Are you sure you want to delete this ad?', 'هل أنت متأكد أنك تريد حذف هذا الإعلان؟')) ?>');">
<input type="hidden" name="action" value="delete_video"> <input type="hidden" name="action" value="delete_media">
<input type="hidden" name="video_id" value="<?= (int) $ad['id'] ?>"> <input type="hidden" name="ad_id" value="<?= (int) $ad['id'] ?>">
<button type="submit" class="btn btn-sm btn-outline-danger"><?= qh_h(qh_t('Delete', 'حذف')) ?></button> <button type="submit" class="btn btn-sm btn-outline-danger"><?= qh_h(qh_t('Delete', 'حذف')) ?></button>
</form> </form>
</div> </div>

View File

@ -6,11 +6,68 @@ qh_boot();
$activeCalls = qh_fetch_tickets(['called', 'in_progress', 'nursing_called'], null, 8); $activeCalls = qh_fetch_tickets(['called', 'in_progress', 'nursing_called'], null, 8);
$queueOverview = qh_queue_overview(); $queueOverview = qh_queue_overview();
$activeVideos = []; function qh_display_ads_column_exists(PDO $pdo, string $column): bool
{
static $cache = [];
if (array_key_exists($column, $cache)) {
return $cache[$column];
}
$stmt = $pdo->prepare(
"SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'hospital_ads' AND COLUMN_NAME = ?"
);
$stmt->execute([DB_NAME, $column]);
return $cache[$column] = ((int) $stmt->fetchColumn() > 0);
}
$activeAds = [];
try { try {
$stmt = db()->query("SELECT video_path FROM hospital_ads WHERE is_active = 1 ORDER BY sort_order ASC, id DESC "); $pdo = db();
$hasMediaType = qh_display_ads_column_exists($pdo, 'media_type');
$hasMediaPath = qh_display_ads_column_exists($pdo, 'media_path');
if ($hasMediaType && $hasMediaPath) {
$stmt = $pdo->query(
"SELECT id, title,
COALESCE(NULLIF(media_type, ''), 'video') AS media_type,
COALESCE(NULLIF(media_path, ''), NULLIF(video_path, '')) AS media_path
FROM hospital_ads
WHERE is_active = 1
ORDER BY sort_order ASC, id DESC"
);
} elseif ($hasMediaPath) {
$stmt = $pdo->query(
"SELECT id, title, 'video' AS media_type,
COALESCE(NULLIF(media_path, ''), NULLIF(video_path, '')) AS media_path
FROM hospital_ads
WHERE is_active = 1
ORDER BY sort_order ASC, id DESC"
);
} else {
$stmt = $pdo->query(
"SELECT id, title, 'video' AS media_type, video_path AS media_path
FROM hospital_ads
WHERE is_active = 1
ORDER BY sort_order ASC, id DESC"
);
}
if ($stmt) { if ($stmt) {
$activeVideos = $stmt->fetchAll(PDO::FETCH_COLUMN); foreach ($stmt->fetchAll() as $ad) {
$mediaPath = (string) ($ad['media_path'] ?? '');
if ($mediaPath === '') {
continue;
}
$activeAds[] = [
'id' => (int) ($ad['id'] ?? 0),
'title' => (string) ($ad['title'] ?? ''),
'type' => (($ad['media_type'] ?? 'video') === 'image') ? 'image' : 'video',
'path' => $mediaPath,
'duration' => 10,
];
}
} }
} catch (Throwable $e) { } catch (Throwable $e) {
// Table might not exist yet, safe to ignore // Table might not exist yet, safe to ignore
@ -189,108 +246,193 @@ qh_page_start(
<div class="col-xl-4 col-lg-5"> <div class="col-xl-4 col-lg-5">
<div class="sticky-top" style="top: 1rem; height: 60vh;"> <div class="sticky-top" style="top: 1rem; height: 60vh;">
<?php if (!empty($activeVideos)): ?> <?php if (!empty($activeAds)): ?>
<div class="card shadow-sm border-0 h-100 bg-black overflow-hidden d-flex justify-content-center align-items-center"> <div class="card shadow-sm border-0 h-100 bg-black overflow-hidden d-flex justify-content-center align-items-center">
<video id="adsVideoPlayer" class="w-100 h-100 object-fit-contain" src="<?= qh_h($activeVideos[0]) ?>" autoplay muted playsinline> <div class="w-100 h-100 position-relative bg-black">
<video id="adsVideoPlayer" class="w-100 h-100 object-fit-contain d-none" muted playsinline></video>
</video> <img id="adsImagePlayer" class="w-100 h-100 object-fit-contain d-none" src="" alt="<?= qh_h(qh_t('Hospital advertisement', 'إعلان المستشفى')) ?>">
</div>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const videos = <?= json_encode($activeVideos, JSON_UNESCAPED_SLASHES) ?>; const playlist = <?= json_encode($activeAds, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
const player = document.getElementById('adsVideoPlayer'); const player = document.getElementById('adsVideoPlayer');
const image = document.getElementById('adsImagePlayer');
if (videos.length === 0) return; const defaultImageAlt = "<?= qh_h(qh_t('Hospital advertisement', 'إعلان المستشفى')) ?>";
let imageTimer = null;
let currentIdx = parseInt(sessionStorage.getItem('adsVideoIndex') || '0', 10); let savedType = sessionStorage.getItem('adsMediaType') || 'video';
let currentIdx = parseInt(sessionStorage.getItem('adsMediaIndex') || sessionStorage.getItem('adsVideoIndex') || '0', 10);
let currentTime = parseFloat(sessionStorage.getItem('adsVideoTime') || '0'); let currentTime = parseFloat(sessionStorage.getItem('adsVideoTime') || '0');
let imageStartedAt = parseInt(sessionStorage.getItem('adsImageStartedAt') || '0', 10);
if (isNaN(currentIdx) || currentIdx < 0 || currentIdx >= videos.length) { if (!player || !image || playlist.length === 0) {
return;
}
if (isNaN(currentIdx) || currentIdx < 0 || currentIdx >= playlist.length) {
currentIdx = 0; currentIdx = 0;
}
if (isNaN(currentTime) || currentTime < 0) {
currentTime = 0; currentTime = 0;
} }
if (isNaN(imageStartedAt) || imageStartedAt < 0) {
imageStartedAt = 0;
}
player.src = videos[currentIdx]; const persistIndex = function() {
player.load(); sessionStorage.setItem('adsMediaIndex', String(currentIdx));
player.addEventListener('loadedmetadata', function() { sessionStorage.setItem('adsVideoIndex', String(currentIdx));
if (currentTime > 0) { };
player.currentTime = isNaN(currentTime) ? 0 : currentTime;
}
}, { once: true });
if (window.localStorage.getItem('hospitalQueue:audioEnabled') !== 'false') { const clearImageTimer = function() {
if (imageTimer) {
clearTimeout(imageTimer);
imageTimer = null;
}
};
const showAudioPrompt = function() {
if (window.audioPromptShown || window.localStorage.getItem('hospitalQueue:audioEnabled') === 'false') {
return;
}
window.audioPromptShown = true;
const prompt = document.createElement('div');
prompt.style.position = 'fixed';
prompt.style.bottom = '20px';
prompt.style.left = '50%';
prompt.style.transform = 'translateX(-50%)';
prompt.style.zIndex = '9999';
prompt.style.backgroundColor = '#dc3545';
prompt.style.color = 'white';
prompt.style.padding = '15px 30px';
prompt.style.borderRadius = '50px';
prompt.style.cursor = 'pointer';
prompt.style.display = 'flex';
prompt.style.alignItems = 'center';
prompt.style.gap = '15px';
prompt.style.boxShadow = '0 10px 25px rgba(0,0,0,0.5)';
const msg = "<?= qh_h(qh_t('Sound blocked by TV/Browser. Click here or press OK on remote to enable.', 'تم حظر الصوت بواسطة المتصفح/التلفاز. انقر هنا أو اضغط OK في الريموت للتفعيل.')) ?>";
prompt.innerHTML = '<i class="bi bi-volume-mute-fill fs-3"></i> <span class="fs-6 fw-bold">' + msg + '</span>';
const enableSound = function(ev) {
if (ev && ev.type === 'keydown' && ev.key !== 'Enter' && ev.key !== ' ') return;
player.muted = false; player.muted = false;
} player.play().catch(e => console.warn('Still cannot play:', e));
if (document.body.contains(prompt)) {
document.body.removeChild(prompt);
}
if (ev && ev.type === 'keydown') {
document.body.click();
}
};
player.play().catch(function(e) { prompt.addEventListener('click', enableSound);
console.error("Error playing video:", e); document.body.appendChild(prompt);
document.addEventListener('keydown', enableSound);
document.addEventListener('click', function(ev2) {
if (document.body.contains(prompt) && ev2.target !== prompt && !prompt.contains(ev2.target)) {
enableSound(ev2);
}
}, { once: true });
};
const playVideo = function() {
player.play().catch(function(e) {
console.error('Error playing video:', e);
if (!player.muted) { if (!player.muted) {
player.muted = true; player.muted = true;
player.play().catch(err => console.error("Fallback play failed:", err)); player.play().catch(err => console.error('Fallback play failed:', err));
} }
showAudioPrompt();
if (!window.audioPromptShown && window.localStorage.getItem('hospitalQueue:audioEnabled') !== 'false') {
window.audioPromptShown = true;
const prompt = document.createElement('div');
prompt.style.position = 'fixed';
prompt.style.bottom = '20px';
prompt.style.left = '50%';
prompt.style.transform = 'translateX(-50%)';
prompt.style.zIndex = '9999';
prompt.style.backgroundColor = '#dc3545'; // danger red
prompt.style.color = 'white';
prompt.style.padding = '15px 30px';
prompt.style.borderRadius = '50px';
prompt.style.cursor = 'pointer';
prompt.style.display = 'flex';
prompt.style.alignItems = 'center';
prompt.style.gap = '15px';
prompt.style.boxShadow = '0 10px 25px rgba(0,0,0,0.5)';
const msg = "<?= qh_h(qh_t('Sound blocked by TV/Browser. Click here or press OK on remote to enable.', 'تم حظر الصوت بواسطة المتصفح/التلفاز. انقر هنا أو اضغط OK في الريموت للتفعيل.')) ?>";
prompt.innerHTML = '<i class="bi bi-volume-mute-fill fs-3"></i> <span class="fs-6 fw-bold">' + msg + '</span>';
const enableSound = function(ev) {
if (ev && ev.type === 'keydown' && ev.key !== 'Enter' && ev.key !== ' ') return;
player.muted = false;
player.play().catch(e => console.warn("Still cannot play:", e));
if (document.body.contains(prompt)) {
document.body.removeChild(prompt);
}
if (ev && ev.type === 'keydown') {
document.body.click();
}
};
prompt.addEventListener('click', enableSound);
document.body.appendChild(prompt);
document.addEventListener('keydown', enableSound);
// Also listen for any click anywhere to dismiss it
document.addEventListener('click', function(ev2) {
if (document.body.contains(prompt) && ev2.target !== prompt && !prompt.contains(ev2.target)) {
enableSound(ev2);
}
}, { once: true });
}
});
if (videos.length > 1) {
player.addEventListener('ended', function() {
currentIdx = (currentIdx + 1) % videos.length;
player.src = videos[currentIdx];
player.load();
player.play().catch(function(e) { console.error("Error playing video:", e); });
}); });
} else { };
player.loop = true;
} const showNextItem = function() {
clearImageTimer();
currentIdx = (currentIdx + 1) % playlist.length;
currentTime = 0;
imageStartedAt = 0;
savedType = 'video';
renderCurrent(true);
};
const renderCurrent = function(resetPosition) {
const item = playlist[currentIdx] || playlist[0];
persistIndex();
clearImageTimer();
sessionStorage.setItem('adsMediaType', item.type);
savedType = item.type;
if (item.type === 'image') {
player.pause();
player.removeAttribute('src');
player.load();
player.classList.add('d-none');
image.classList.remove('d-none');
image.src = item.path;
image.alt = item.title || defaultImageAlt;
const durationMs = Math.max(parseInt(item.duration || 10, 10), 3) * 1000;
let remainingMs = durationMs;
const canResumeImage = !resetPosition && imageStartedAt > 0;
if (!canResumeImage) {
imageStartedAt = Date.now();
} else {
const elapsedMs = Date.now() - imageStartedAt;
if (elapsedMs >= durationMs) {
showNextItem();
return;
}
remainingMs = durationMs - elapsedMs;
}
sessionStorage.setItem('adsImageStartedAt', String(imageStartedAt));
sessionStorage.setItem('adsVideoTime', '0');
imageTimer = window.setTimeout(showNextItem, remainingMs);
return;
}
image.classList.add('d-none');
player.classList.remove('d-none');
imageStartedAt = 0;
sessionStorage.removeItem('adsImageStartedAt');
const resumeTime = (!resetPosition && currentTime > 0) ? currentTime : 0;
player.loop = playlist.length === 1;
player.src = item.path;
player.load();
player.addEventListener('loadedmetadata', function onLoadedMetadata() {
if (resumeTime > 0 && resumeTime < player.duration) {
player.currentTime = resumeTime;
}
playVideo();
}, { once: true });
if (window.localStorage.getItem('hospitalQueue:audioEnabled') !== 'false') {
player.muted = false;
}
};
player.addEventListener('ended', function() {
if (playlist.length > 1) {
showNextItem();
}
});
window.addEventListener('beforeunload', function() { window.addEventListener('beforeunload', function() {
sessionStorage.setItem('adsVideoIndex', currentIdx); persistIndex();
sessionStorage.setItem('adsVideoTime', player.currentTime); sessionStorage.setItem('adsMediaType', savedType);
if (savedType === 'video') {
sessionStorage.setItem('adsVideoTime', String(player.currentTime || 0));
} else {
sessionStorage.setItem('adsVideoTime', '0');
sessionStorage.setItem('adsImageStartedAt', String(imageStartedAt || Date.now()));
}
}); });
renderCurrent(false);
}); });
</script> </script>
</div> </div>