39414-vm/display.php
2026-04-02 17:14:46 +00:00

237 lines
13 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/queue_bootstrap.php';
qh_boot();
$activeCalls = qh_fetch_tickets(['called', 'in_progress'], null, 8);
$queueOverview = qh_queue_overview();
$activeVideos = [];
try {
$stmt = db()->query("SELECT video_path FROM hospital_ads WHERE is_active = 1 ORDER BY sort_order ASC, id DESC ");
if ($stmt) {
$activeVideos = $stmt->fetchAll(PDO::FETCH_COLUMN);
}
} catch (Throwable $e) {
// Table might not exist yet, safe to ignore
}
$activeNews = [];
try {
$stmt = db()->query("SELECT phrase FROM hospital_news WHERE is_active = 1 ORDER BY sort_order ASC, id DESC");
if ($stmt) {
$activeNews = $stmt->fetchAll(PDO::FETCH_COLUMN);
}
} catch (Throwable $e) {
// Table might not exist yet
}
qh_page_start(
'display',
qh_t('General display board', 'لوحة العرض العامة'),
qh_t('Public queue display.', 'شاشة طوابير عامة.')
);
?>
<style>
/* Remove top spacing from the shell wrapper */
main.app-shell {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
</style>
<div class="container-fluid px-0 px-lg-0 py-0 m-0" data-auto-refresh="20" style="min-height: 100vh; display: flex; flex-direction: column; ">
<!-- Top Header for Display Board -->
<header class="d-flex justify-content-between align-items-center bg-white py-2 px-3 shadow-sm border-0">
<div class="d-flex align-items-center gap-3">
<?php if ($logoUrl = qh_hospital_logo_url()): ?>
<img src="<?= qh_h($logoUrl) ?>" alt="<?= qh_h(qh_hospital_name()) ?>" style="height: 50px;">
<?php else: ?>
<div class="bg-primary text-white rounded d-flex align-items-center justify-content-center fs-4 fw-bold" style="width: 50px; height: 50px;">
<?= qh_h(qh_hospital_brand_initials()) ?>
</div>
<?php endif; ?>
<div>
<h1 class="h3 mb-0 fw-bold text-primary"><?= qh_h(qh_hospital_name()) ?></h1>
<?php if ($tagline = qh_hospital_tagline()): ?>
<div class="text-muted fw-semibold"><?= qh_h($tagline) ?></div>
<?php endif; ?>
</div>
</div>
<div class="d-flex align-items-center gap-3">
<div class="fs-5 fw-bold text-dark js-live-clock"><?= qh_h(date('H:i')) ?></div>
<button type="button" class="btn btn-outline-secondary btn-sm shadow-sm me-2 d-flex align-items-center gap-1 js-audio-toggle" id="globalAudioToggle" title="<?= qh_h(qh_t('Toggle Audio', 'تبديل الصوت')) ?>">
<i class="bi bi-megaphone"></i> <span class="d-none d-sm-inline"><?= qh_h(qh_t('Sound', 'الصوت')) ?></span>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm shadow-sm d-flex align-items-center gap-1 js-fullscreen-toggle" aria-pressed="false" data-label-enter="<?= qh_h(qh_t('Fullscreen', 'ملء الشاشة')) ?>" data-label-exit="<?= qh_h(qh_t('Exit fullscreen', 'إنهاء ملء الشاشة')) ?>"><i class="bi bi-arrows-fullscreen"></i> <span class="d-none d-sm-inline"><?= qh_h(qh_t('Fullscreen', 'ملء الشاشة')) ?></span></button>
</div>
</header>
<?php $pb = !empty($activeNews) ? "padding-bottom: 4rem !important;" : "padding-bottom: 1.5rem !important;"; ?>
<div class="row g-4 m-0 mt-2 px-3 px-lg-4 pt-0 flex-grow-1" style="<?= $pb ?>">
<div class="col-xl-8 col-lg-7 d-flex flex-column gap-4" id="queueSections">
<div class="card shadow-sm border-0 flex-grow-1">
<div class="card-header bg-white border-bottom py-2 d-flex justify-content-between align-items-center">
<div>
<h2 class="h4 mb-0 text-gray-800 fw-bold"><?= qh_h(qh_t('Now Serving', 'يتم الآن النداء')) ?></h2>
</div>
</div>
<div class="card-body p-4 bg-light">
<?php if ($activeCalls): ?>
<div class="row g-3 row-cols-2 row-cols-lg-5">
<?php foreach ($activeCalls as $ticket): $speech = qh_call_message($ticket); ?>
<div class="col">
<div class="card border-0 shadow-sm h-100 announcement-card" data-announcement-key="<?= qh_h((string) $ticket['id']) ?>-<?= qh_h((string) strtotime((string) $ticket['called_at'])) ?>" data-announcement-en="<?= qh_h($speech['speech_en'] ?? $speech['en']) ?>" data-announcement-ar="<?= qh_h($speech['speech_ar'] ?? $speech['ar']) ?>">
<div class="card-body w-100 text-center p-2 d-flex flex-column align-items-center justify-content-center">
<div class="fs-2 fw-bold text-primary mb-1"><?= qh_h($ticket['ticket_number']) ?></div>
<div class="small text-muted mb-1 text-truncate w-100" title="<?= qh_h(qh_name($ticket, 'doctor_name', qh_t('Doctor', 'الطبيب'))) ?>"><?= qh_h(qh_name($ticket, 'doctor_name', qh_t('Doctor', 'الطبيب'))) ?></div>
<div class="bg-primary text-white rounded px-2 py-1 small fw-bold mt-1 mb-2">
<?= qh_h(qh_t('Room', 'غرفة')) ?> <?= qh_h($ticket['doctor_room'] ?? '--') ?>
</div>
<div class="text-muted mt-auto" style="font-size: 0.65rem;">
<?= qh_format_datetime($ticket['called_at'] ?? $ticket['updated_at']) ?>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="d-flex align-items-center justify-content-center h-100 min-vh-50 text-muted">
<div class="text-center">
<i class="bi bi-display display-1 mb-3 opacity-50"></i>
<h2><?= qh_h(qh_t('No active calls right now.', 'لا توجد نداءات نشطة حالياً.')) ?></h2>
<p class="lead opacity-75"><?= qh_h(qh_t('Please wait for your ticket number.', 'يرجى الانتظار حتى يتم نداء رقم تذكرتك.')) ?></p>
</div>
</div>
<?php endif; ?>
</div>
</div>
<div class="card shadow-sm border-0">
<div class="card-header bg-white border-bottom py-2">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Queue by Clinic', 'الإنتظار حسب العيادة')) ?></h5>
</div>
<div class="card-body p-4 bg-light">
<div class="row g-3 row-cols-2 row-cols-lg-5">
<?php foreach ($queueOverview as $row): ?>
<div class="col">
<div class="card border-0 shadow-sm h-100">
<div class="card-body p-2 d-flex flex-column justify-content-center">
<h6 class="fw-bold text-primary text-center mb-2 text-truncate" title="<?= qh_h(qh_name($row)) ?>"><?= qh_h(qh_name($row)) ?></h6>
<?php if ((int) ($row['requires_vitals'] ?? 0) === 1): ?>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted fw-semibold lh-sm" style="font-size: 0.7rem;"><?= qh_h(qh_t('Vitals Wait', 'غرفة المعاينة الأولية')) ?></span>
<span class="badge bg-warning text-dark rounded-pill px-2"><?= qh_h((string) $row['vitals_waiting']) ?></span>
</div>
<?php endif; ?>
<div class="d-flex justify-content-between align-items-center">
<span class="text-muted fw-semibold lh-sm" style="font-size: 0.7rem;"><?= qh_h(qh_t('Doctor Wait', 'انتظار الطبيب')) ?></span>
<span class="badge bg-info text-dark rounded-pill px-2"><?= qh_h((string) $row['doctor_waiting']) ?></span>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-5">
<div class="sticky-top" style="top: 1rem; height: 60vh;">
<?php if (!empty($activeVideos)): ?>
<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" autoplay muted playsinline>
<source src="<?= qh_h($activeVideos[0]) ?>" type="video/mp4">
</video>
<script>
document.addEventListener('DOMContentLoaded', function() {
const videos = <?= json_encode($activeVideos, JSON_UNESCAPED_SLASHES) ?>;
const player = document.getElementById('adsVideoPlayer');
if (videos.length === 0) return;
let currentIdx = parseInt(sessionStorage.getItem('adsVideoIndex') || '0', 10);
let currentTime = parseFloat(sessionStorage.getItem('adsVideoTime') || '0');
if (isNaN(currentIdx) || currentIdx < 0 || currentIdx >= videos.length) {
currentIdx = 0;
currentTime = 0;
}
player.src = videos[currentIdx];
player.currentTime = isNaN(currentTime) ? 0 : currentTime;
if (window.localStorage.getItem('hospitalQueue:audioEnabled') === 'true') {
player.muted = false;
}
player.play().catch(function(e) {
console.error("Error playing video:", e);
if (!player.muted) {
player.muted = true;
player.play().catch(e => console.error("Fallback play failed:", e));
}
});
if (videos.length > 1) {
player.addEventListener('ended', function() {
currentIdx = (currentIdx + 1) % videos.length;
player.src = videos[currentIdx];
player.play().catch(function(e) { console.error("Error playing video:", e); });
});
} else {
player.loop = true;
}
window.addEventListener('beforeunload', function() {
sessionStorage.setItem('adsVideoIndex', currentIdx);
sessionStorage.setItem('adsVideoTime', player.currentTime);
});
});
</script>
</div>
<?php else: ?>
<div class="card shadow-sm border-0 h-100 bg-primary text-white">
<div class="card-header border-bottom border-light border-opacity-25 py-2 bg-transparent d-flex justify-content-between align-items-center">
<h5 class="mb-0 font-weight-bold text-white"><i class="bi bi-info-circle me-2"></i><?= qh_h(qh_t('Information', 'معلومات')) ?></h5>
<span class="badge bg-white text-primary rounded-pill small"><?= qh_h(qh_t('Notices', 'تنبيهات')) ?></span>
</div>
<div class="card-body p-4 d-flex flex-column gap-4">
<div class="border border-light border-opacity-25 rounded p-4 bg-white bg-opacity-10">
<div class="badge bg-light text-primary mb-2 px-2 py-1"><?= qh_h(qh_t('Service', 'خدمة')) ?></div>
<h4 class="text-white fw-bold"><?= qh_h(qh_t('Lab packages & checks', 'باقات المختبر والفحوصات')) ?></h4>
<p class="mb-0 text-white text-opacity-75"><?= qh_h(qh_t('Ask reception about bundled blood tests, diabetes follow-up, and annual screenings.', 'اسأل الاستقبال عن باقات تحاليل الدم، ومتابعة السكري، والفحوصات السنوية.')) ?></p>
</div>
<div class="border border-light border-opacity-25 rounded p-4 bg-white bg-opacity-10">
<div class="badge bg-light text-primary mb-2 px-2 py-1"><?= qh_h(qh_t('Reminder', 'تذكير')) ?></div>
<h4 class="text-white fw-bold"><?= qh_h(qh_t('Keep your ticket visible', 'احتفظ بتذكرتك ظاهرة')) ?></h4>
<p class="mb-0 text-white text-opacity-75"><?= qh_h(qh_t('We announce ticket numbers on this screen and by voice. Stay near your department area.', 'نعلن أرقام التذاكر على هذه الشاشة وبالصوت. يرجى البقاء قرب منطقة القسم الخاص بك.')) ?></p>
</div>
<div class="border border-light border-opacity-25 rounded p-4 bg-white bg-opacity-10">
<div class="badge bg-light text-primary mb-2 px-2 py-1"><?= qh_h(qh_t('Wayfinding', 'الإرشاد')) ?></div>
<h4 class="text-white fw-bold"><?= qh_h(qh_t('Pharmacy & billing', 'الصيدلية والمحاسبة')) ?></h4>
<p class="mb-0 text-white text-opacity-75"><?= qh_h(qh_t('Completed visits can proceed to the pharmacy and billing desk near the main exit.', 'بعد انتهاء الزيارة يمكن التوجه إلى الصيدلية ومكتب المحاسبة قرب المخرج الرئيسي.')) ?></p>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php if (!empty($activeNews)): ?>
<div class="news-ticker-container">
<div class="news-ticker-content">
<?php foreach ($activeNews as $news): ?>
<span class="news-ticker-item"><?= qh_h($news) ?></span>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php qh_page_end(); ?>