Autosave: 20260331-103916

This commit is contained in:
Flatlogic Bot 2026-03-31 10:39:17 +00:00
parent fd3feb7878
commit 0c7ba978a8
13 changed files with 2289 additions and 550 deletions

285
admin.php
View File

@ -4,163 +4,158 @@ require_once __DIR__ . '/queue_bootstrap.php';
qh_boot(); qh_boot();
qh_admin_handle_request(); qh_admin_handle_request();
$stats = qh_admin_stats();
$clinics = qh_fetch_clinics(); $clinics = qh_fetch_clinics();
$doctors = qh_fetch_doctors(); $doctors = qh_fetch_doctors();
$recentClinics = array_slice($clinics, 0, 4);
$recentDoctors = array_slice($doctors, 0, 5);
$profile = qh_fetch_hospital_profile();
qh_page_start('admin', 'Admin configuration', 'Configure clinics, doctors, room numbers, and vitals requirements for the queue workflow.'); qh_page_start(
'admin',
qh_t('Admin overview', 'نظرة عامة للإدارة'),
qh_t('Structured admin overview with separate pages for clinics and doctors.', 'نظرة عامة منظمة للإدارة مع صفحات مستقلة للعيادات والأطباء.')
);
?> ?>
<div class="container-xxl px-3 px-lg-4"> <div class="container-xxl px-3 px-lg-4">
<section class="page-header-panel mb-4"> <div class="admin-layout">
<div> <aside class="admin-sidebar-column">
<span class="section-kicker">Admin / الإدارة</span> <?php qh_render_admin_sidebar('admin.php', $stats); ?>
<h1 class="section-title-xl mt-2">Clinic and doctor setup.</h1> </aside>
<p class="section-copy mb-0">This first iteration supports bilingual clinic names, a vitals-required flag, doctor-room assignments, and immediate use by reception.</p>
</div>
</section>
<div class="row g-4 mb-4"> <div class="admin-content-stack">
<div class="col-xl-5"> <section class="page-header-panel admin-hero-panel mb-0">
<div class="panel-card h-100"> <div>
<h2 class="section-title mb-3">Add clinic / إضافة عيادة</h2> <span class="section-kicker"><?= qh_h(qh_t('Admin overview', 'نظرة عامة للإدارة')) ?></span>
<form method="post" class="vstack gap-3"> <div class="locale-chip mt-3"><?= qh_h(qh_current_language_badge()) ?></div>
<input type="hidden" name="action" value="add_clinic"> <h1 class="section-title-xl mt-2 mb-2"><?= qh_h(qh_t('A cleaner admin center with separate management pages.', 'مركز إدارة أنظف مع صفحات إدارة منفصلة.')) ?></h1>
<div> <p class="section-copy mb-0"><?= qh_h(qh_t('Use the left sidebar to open focused pages for clinics and doctors instead of working in one long page.', 'استخدم الشريط الجانبي لفتح صفحات مركزة للعيادات والأطباء بدلاً من العمل داخل صفحة واحدة طويلة.')) ?></p>
<label class="form-label">Code</label> </div>
<input class="form-control" type="text" name="code" maxlength="10" placeholder="GEN" required> </section>
</div>
<div>
<label class="form-label">Name (English)</label>
<input class="form-control" type="text" name="name_en" placeholder="General Medicine" required>
</div>
<div>
<label class="form-label">Name (Arabic)</label>
<input class="form-control" type="text" name="name_ar" placeholder="الطب العام" required>
</div>
<div class="row g-3">
<div class="col-sm-6">
<label class="form-label">Sort order</label>
<input class="form-control" type="number" name="sort_order" value="50" min="1">
</div>
<div class="col-sm-6 d-flex align-items-end">
<div class="form-check form-switch border rounded p-3 w-100 bg-light-subtle">
<input class="form-check-input" type="checkbox" role="switch" id="requiresVitalsCreate" name="requires_vitals" checked>
<label class="form-check-label" for="requiresVitalsCreate">Requires vitals first / يتطلب العلامات الحيوية أولاً</label>
</div>
</div>
</div>
<button class="btn btn-dark" type="submit">Save clinic</button>
</form>
</div>
</div>
<div class="col-xl-7">
<div class="panel-card h-100">
<h2 class="section-title mb-3">Add doctor / إضافة طبيب</h2>
<form method="post" class="row g-3 align-items-end">
<input type="hidden" name="action" value="add_doctor">
<div class="col-md-6">
<label class="form-label">Name (English)</label>
<input class="form-control" type="text" name="name_en" placeholder="Dr. Sarah Malik" required>
</div>
<div class="col-md-6">
<label class="form-label">Name (Arabic)</label>
<input class="form-control" type="text" name="name_ar" placeholder="د. سارة مالك" required>
</div>
<div class="col-md-5">
<label class="form-label">Clinic</label>
<select class="form-select" name="clinic_id" required>
<option value="">Choose clinic</option>
<?php foreach ($clinics as $clinic): ?>
<option value="<?= qh_h((string) $clinic['id']) ?>"><?= qh_h($clinic['name_en']) ?> / <?= qh_h($clinic['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Room</label>
<input class="form-control" type="text" name="room_number" placeholder="201" required>
</div>
<div class="col-md-2">
<label class="form-label">Order</label>
<input class="form-control" type="number" name="sort_order" value="50" min="1">
</div>
<div class="col-md-2">
<button class="btn btn-dark w-100" type="submit">Save</button>
</div>
</form>
</div>
</div>
</div>
<div class="row g-4"> <section class="panel-card admin-section-card">
<div class="col-xl-5"> <div class="admin-section-head">
<div class="panel-card h-100"> <div>
<h2 class="section-title mb-3">Clinics / العيادات</h2> <span class="section-kicker"><?= qh_h(qh_t('System snapshot', 'ملخص النظام')) ?></span>
<div class="vstack gap-3"> <h2 class="section-title mt-2 mb-1"><?= qh_h(qh_t('Current queue configuration', 'إعداد نظام الطوابير الحالي')) ?></h2>
<?php foreach ($clinics as $clinic): ?> <p class="section-copy mb-0"><?= qh_h(qh_t('Quick numbers and shortcuts for the main admin tasks.', 'أرقام سريعة واختصارات لمهام الإدارة الأساسية.')) ?></p>
<form method="post" class="list-row-form"> </div>
<input type="hidden" name="action" value="update_clinic"> </div>
<input type="hidden" name="clinic_id" value="<?= qh_h((string) $clinic['id']) ?>">
<div class="admin-overview-grid mt-4">
<article class="mini-overview-card admin-overview-card"><span class="workflow-step"><?= qh_h(qh_t('Clinics', 'العيادات')) ?></span><strong><?= qh_h((string) $stats['clinics']) ?></strong><p><?= qh_h(qh_t('Configured destinations used by reception and routing.', 'الوجهات المهيأة التي يستخدمها الاستقبال ومسار المريض.')) ?></p></article>
<article class="mini-overview-card admin-overview-card"><span class="workflow-step"><?= qh_h(qh_t('Doctors', 'الأطباء')) ?></span><strong><?= qh_h((string) $stats['doctors']) ?></strong><p><?= qh_h(qh_t('Doctors currently assigned to rooms and clinics.', 'الأطباء المعينون حالياً للغرف والعيادات.')) ?></p></article>
<article class="mini-overview-card admin-overview-card"><span class="workflow-step"><?= qh_h(qh_t('Vitals first', 'العلامات أولاً')) ?></span><strong><?= qh_h((string) $stats['vitals_clinics']) ?></strong><p><?= qh_h(qh_t('Clinics that must pass through nursing before the doctor.', 'العيادات التي تمر على التمريض قبل الطبيب.')) ?></p></article>
<article class="mini-overview-card admin-overview-card"><span class="workflow-step"><?= qh_h(qh_t('Direct doctor', 'الطبيب مباشرة')) ?></span><strong><?= qh_h((string) $stats['direct_clinics']) ?></strong><p><?= qh_h(qh_t('Clinics that route patients directly to the doctor queue.', 'العيادات التي توجه المرضى مباشرة إلى انتظار الطبيب.')) ?></p></article>
</div>
</section>
<section class="admin-card-grid admin-card-grid-triple">
<article class="panel-card admin-link-card">
<span class="section-kicker"><?= qh_h(qh_t('Branding', 'الهوية')) ?></span>
<h2 class="section-title mt-2 mb-2"><?= qh_h(qh_t('Hospital profile', 'ملف المستشفى')) ?></h2>
<p class="section-copy mb-4"><?= qh_h(qh_t('Update the hospital logo, favicon, contact details, working hours, and brand colors from a dedicated profile page.', 'حدّث شعار المستشفى والأيقونة وبيانات التواصل وساعات العمل وألوان الهوية من صفحة ملف مستقلة.')) ?></p>
<a class="btn btn-dark" href="<?= qh_h(qh_url('admin_hospital.php')) ?>"><?= qh_h(qh_t('Open profile page', 'فتح صفحة الملف')) ?></a>
</article>
<article class="panel-card admin-link-card">
<span class="section-kicker"><?= qh_h(qh_t('Directory', 'الدليل')) ?></span>
<h2 class="section-title mt-2 mb-2"><?= qh_h(qh_t('Manage clinics', 'إدارة العيادات')) ?></h2>
<p class="section-copy mb-4"><?= qh_h(qh_t('Create clinics, define codes, set routing, and edit the display order from a dedicated page.', 'أنشئ العيادات وحدد الرموز ومسار العمل وعدّل ترتيب العرض من صفحة مستقلة.')) ?></p>
<a class="btn btn-dark" href="<?= qh_h(qh_url('admin_clinics.php')) ?>"><?= qh_h(qh_t('Open clinics page', 'فتح صفحة العيادات')) ?></a>
</article>
<article class="panel-card admin-link-card">
<span class="section-kicker"><?= qh_h(qh_t('Directory', 'الدليل')) ?></span>
<h2 class="section-title mt-2 mb-2"><?= qh_h(qh_t('Manage doctors', 'إدارة الأطباء')) ?></h2>
<p class="section-copy mb-4"><?= qh_h(qh_t('Search the doctor list, update room assignments, and manage records with edit and delete actions.', 'ابحث في قائمة الأطباء وحدّث الغرف والتعيينات وأدر السجلات بخيارات التعديل والحذف.')) ?></p>
<a class="btn btn-dark" href="<?= qh_h(qh_url('admin_doctors.php')) ?>"><?= qh_h(qh_t('Open doctors page', 'فتح صفحة الأطباء')) ?></a>
</article>
</section>
<div class="admin-card-grid admin-card-grid-secondary admin-card-grid-triple">
<section class="panel-card admin-section-card hospital-overview-card">
<div class="admin-section-head">
<div>
<span class="section-kicker"><?= qh_h(qh_t('Hospital profile', 'ملف المستشفى')) ?></span>
<h2 class="section-title mt-2 mb-1"><?= qh_h(qh_hospital_name()) ?></h2>
</div>
<a class="btn btn-sm btn-outline-dark" href="<?= qh_h(qh_url('admin_hospital.php')) ?>"><?= qh_h(qh_t('Edit profile', 'تعديل الملف')) ?></a>
</div>
<div class="vstack gap-3 mt-4">
<div class="admin-summary-row">
<div> <div>
<div class="fw-semibold"><?= qh_h($clinic['name_en']) ?></div> <div class="fw-semibold"><?= qh_h(qh_t('Short name', 'الاسم المختصر')) ?></div>
<div class="small text-secondary" lang="ar" dir="rtl"><?= qh_h($clinic['name_ar']) ?></div> <div class="small text-secondary"><?= qh_h((string) ($profile['short_name'] ?? qh_t('Not set yet', 'غير محدد بعد'))) ?></div>
<div class="small text-secondary mt-1">Code <?= qh_h($clinic['code']) ?></div>
</div> </div>
<div class="row g-2 align-items-center mt-1"> <span class="table-pill info"><?= qh_h(qh_t('Brand ready', 'الهوية جاهزة')) ?></span>
<div class="col-sm-6"> </div>
<label class="form-label small mb-1">Sort order</label> <div class="admin-summary-row">
<input class="form-control form-control-sm" type="number" name="sort_order" value="<?= qh_h((string) $clinic['sort_order']) ?>" min="1"> <div>
</div> <div class="fw-semibold"><?= qh_h(qh_t('Phone', 'الهاتف')) ?></div>
<div class="col-sm-6"> <div class="small text-secondary"><?= qh_h((string) (($profile['phone'] ?? '') !== '' ? $profile['phone'] : qh_t('Add a contact number', 'أضف رقم تواصل'))) ?></div>
<div class="form-check form-switch mt-4"> </div>
<input class="form-check-input" type="checkbox" role="switch" id="clinicSwitch<?= qh_h((string) $clinic['id']) ?>" name="requires_vitals" <?= (int) $clinic['requires_vitals'] === 1 ? 'checked' : '' ?>> <span class="room-badge"><?= qh_h(qh_t('Hours', 'الساعات')) ?></span>
<label class="form-check-label small" for="clinicSwitch<?= qh_h((string) $clinic['id']) ?>">Vitals required</label> </div>
<div class="admin-summary-row">
<div>
<div class="fw-semibold"><?= qh_h(qh_t('Working hours', 'ساعات العمل')) ?></div>
<div class="small text-secondary"><?= qh_h(qh_name($profile, 'working_hours', qh_t('Set your opening hours', 'حدّد ساعات العمل'))) ?></div>
</div>
<span class="table-pill dark"><?= qh_h(qh_t('Shared app branding', 'هوية مشتركة للتطبيق')) ?></span>
</div>
</div>
</section>
<section class="panel-card admin-section-card">
<div class="admin-section-head">
<div>
<span class="section-kicker"><?= qh_h(qh_t('Clinic preview', 'معاينة العيادات')) ?></span>
<h2 class="section-title mt-2 mb-1"><?= qh_h(qh_t('Latest clinic setup', 'أحدث إعدادات العيادات')) ?></h2>
</div>
<a class="btn btn-sm btn-outline-dark" href="<?= qh_h(qh_url('admin_clinics.php')) ?>"><?= qh_h(qh_t('View all', 'عرض الكل')) ?></a>
</div>
<?php if ($recentClinics === []): ?>
<div class="empty-state compact mt-4"><strong><?= qh_h(qh_t('No clinics configured yet.', 'لم يتم إعداد أي عيادات بعد.')) ?></strong><span><?= qh_h(qh_t('Open the clinics page to add your first clinic.', 'افتح صفحة العيادات لإضافة أول عيادة.')) ?></span></div>
<?php else: ?>
<div class="vstack gap-3 mt-4">
<?php foreach ($recentClinics as $clinic): ?>
<div class="admin-summary-row">
<div>
<div class="fw-semibold"><?= qh_h(qh_name($clinic)) ?></div>
<div class="small text-secondary"><?= qh_h(qh_t('Code', 'الرمز')) ?>: <?= qh_h((string) $clinic['code']) ?></div>
</div> </div>
<span class="table-pill <?= (int) $clinic['requires_vitals'] === 1 ? 'warning' : 'info' ?>"><?= qh_h((int) $clinic['requires_vitals'] === 1 ? qh_t('Vitals first', 'العلامات أولاً') : qh_t('Direct doctor', 'الطبيب مباشرة')) ?></span>
</div> </div>
</div> <?php endforeach; ?>
<button class="btn btn-sm btn-outline-dark mt-3" type="submit">Update clinic</button> </div>
</form> <?php endif; ?>
<?php endforeach; ?> </section>
</div>
</div> <section class="panel-card admin-section-card">
</div> <div class="admin-section-head">
<div class="col-xl-7"> <div>
<div class="panel-card h-100"> <span class="section-kicker"><?= qh_h(qh_t('Doctor preview', 'معاينة الأطباء')) ?></span>
<h2 class="section-title mb-3">Doctors & rooms / الأطباء والغرف</h2> <h2 class="section-title mt-2 mb-1"><?= qh_h(qh_t('Latest doctor assignments', 'أحدث تعيينات الأطباء')) ?></h2>
<div class="vstack gap-3"> </div>
<?php foreach ($doctors as $doctor): ?> <a class="btn btn-sm btn-outline-dark" href="<?= qh_h(qh_url('admin_doctors.php')) ?>"><?= qh_h(qh_t('View all', 'عرض الكل')) ?></a>
<form method="post" class="list-row-form"> </div>
<input type="hidden" name="action" value="update_doctor"> <?php if ($recentDoctors === []): ?>
<input type="hidden" name="doctor_id" value="<?= qh_h((string) $doctor['id']) ?>"> <div class="empty-state compact mt-4"><strong><?= qh_h(qh_t('No doctors configured yet.', 'لم يتم إعداد أي أطباء بعد.')) ?></strong><span><?= qh_h(qh_t('Open the doctors page to add your first doctor profile.', 'افتح صفحة الأطباء لإضافة أول ملف طبيب.')) ?></span></div>
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3"> <?php else: ?>
<div> <div class="vstack gap-3 mt-4">
<div class="fw-semibold"><?= qh_h($doctor['name_en']) ?></div> <?php foreach ($recentDoctors as $doctor): ?>
<div class="small text-secondary" lang="ar" dir="rtl"><?= qh_h($doctor['name_ar']) ?></div> <div class="admin-summary-row">
<div>
<div class="fw-semibold"><?= qh_h(qh_name($doctor)) ?></div>
<div class="small text-secondary"><?= qh_h(qh_name($doctor, 'clinic_name', qh_t('Unassigned clinic', 'عيادة غير محددة'))) ?></div>
</div>
<span class="room-badge"><?= qh_h(qh_t('Room', 'الغرفة')) ?> <?= qh_h((string) $doctor['room_number']) ?></span>
</div> </div>
<span class="room-badge">Current room <?= qh_h($doctor['room_number']) ?></span> <?php endforeach; ?>
</div> </div>
<div class="row g-3 align-items-end mt-1"> <?php endif; ?>
<div class="col-md-5"> </section>
<label class="form-label small">Clinic</label>
<select class="form-select form-select-sm" name="clinic_id" required>
<?php foreach ($clinics as $clinic): ?>
<option value="<?= qh_h((string) $clinic['id']) ?>" <?= (int) $clinic['id'] === (int) $doctor['clinic_id'] ? 'selected' : '' ?>><?= qh_h($clinic['name_en']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label small">Room</label>
<input class="form-control form-control-sm" type="text" name="room_number" value="<?= qh_h($doctor['room_number']) ?>" required>
</div>
<div class="col-md-2">
<label class="form-label small">Order</label>
<input class="form-control form-control-sm" type="number" name="sort_order" value="<?= qh_h((string) $doctor['sort_order']) ?>" min="1">
</div>
<div class="col-md-2 d-grid">
<button class="btn btn-sm btn-outline-dark" type="submit">Save</button>
</div>
</div>
</form>
<?php endforeach; ?>
</div>
</div> </div>
</div> </div>
</div> </div>

188
admin_clinics.php Normal file
View File

@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/queue_bootstrap.php';
qh_boot();
qh_admin_handle_request();
$stats = qh_admin_stats();
$clinics = qh_fetch_clinics();
$search = trim((string) ($_GET['q'] ?? ''));
$editId = (int) ($_GET['edit'] ?? 0);
$editClinic = $editId > 0 ? qh_fetch_clinic($editId) : null;
if ($editId > 0 && $editClinic === null) {
qh_set_flash('warning', qh_t('The requested clinic record was not found.', 'لم يتم العثور على سجل العيادة المطلوب.'));
qh_redirect('admin_clinics.php');
}
$toLower = static function (string $value): string {
return function_exists('mb_strtolower') ? mb_strtolower($value, 'UTF-8') : strtolower($value);
};
if ($search !== '') {
$needle = $toLower($search);
$clinics = array_values(array_filter($clinics, static function (array $clinic) use ($needle, $toLower): bool {
$haystack = implode(' ', [
(string) ($clinic['code'] ?? ''),
(string) ($clinic['name_en'] ?? ''),
(string) ($clinic['name_ar'] ?? ''),
(int) ($clinic['requires_vitals'] ?? 0) === 1 ? 'vitals first' : 'direct doctor',
]);
return str_contains($toLower($haystack), $needle);
}));
}
qh_page_start(
'admin',
qh_t('Clinic management', 'إدارة العيادات'),
qh_t('Professional clinic directory with search, edit, and delete actions.', 'دليل احترافي للعيادات مع البحث وخيارات التعديل والحذف.')
);
?>
<div class="container-xxl px-3 px-lg-4">
<div class="admin-layout">
<aside class="admin-sidebar-column">
<?php qh_render_admin_sidebar('admin_clinics.php', $stats); ?>
</aside>
<div class="admin-content-stack">
<section class="page-header-panel admin-hero-panel mb-0">
<div>
<span class="section-kicker"><?= qh_h(qh_t('Clinic directory', 'دليل العيادات')) ?></span>
<div class="locale-chip mt-3"><?= qh_h(qh_current_language_badge()) ?></div>
<h1 class="section-title-xl mt-2 mb-2"><?= qh_h(qh_t('Dedicated clinic management with cleaner routing control.', 'إدارة مستقلة للعيادات مع تحكم أنظف في مسار العمل.')) ?></h1>
<p class="section-copy mb-0"><?= qh_h(qh_t('Search clinics, adjust codes and routing, and keep the front desk workflow organized.', 'ابحث في العيادات وعدّل الرموز ومسار العمل وحافظ على تنظيم سير العمل في الاستقبال.')) ?></p>
</div>
</section>
<section class="panel-card admin-toolbar-card">
<div class="admin-toolbar">
<form method="get" class="admin-search-form" role="search">
<input type="hidden" name="lang" value="<?= qh_h(qh_locale()) ?>">
<label class="visually-hidden" for="clinicSearch"><?= qh_h(qh_t('Search clinics', 'البحث عن العيادات')) ?></label>
<input id="clinicSearch" class="form-control" type="search" name="q" value="<?= qh_h($search) ?>" placeholder="<?= qh_h(qh_t('Search by clinic name or code', 'ابحث باسم العيادة أو الرمز')) ?>">
<button class="btn btn-dark" type="submit"><?= qh_h(qh_t('Search', 'بحث')) ?></button>
<?php if ($search !== ''): ?>
<a class="btn btn-outline-dark" href="<?= qh_h(qh_url('admin_clinics.php')) ?>"><?= qh_h(qh_t('Reset', 'إعادة ضبط')) ?></a>
<?php endif; ?>
</form>
<?php if ($editClinic !== null): ?>
<a class="btn btn-outline-dark" href="<?= qh_h(qh_url('admin_clinics.php')) ?>"><?= qh_h(qh_t('Add new clinic', 'إضافة عيادة جديدة')) ?></a>
<?php endif; ?>
</div>
</section>
<div class="admin-directory-layout">
<section class="panel-card admin-table-card">
<div class="admin-section-head">
<div>
<span class="section-kicker"><?= qh_h(qh_t('Clinic list', 'قائمة العيادات')) ?></span>
<h2 class="section-title mt-2 mb-1"><?= qh_h(qh_t('Clinics and routing rules', 'العيادات وقواعد مسار العمل')) ?></h2>
<p class="section-copy mb-0"><?= qh_h($search !== ''
? qh_t('Filtered results based on your search.', 'نتائج مفلترة بناءً على البحث.')
: qh_t('All configured clinic records.', 'جميع سجلات العيادات المهيأة.')) ?></p>
</div>
<div class="admin-count-chip"><?= qh_h((string) count($clinics)) ?> <?= qh_h(qh_t('records', 'سجلات')) ?></div>
</div>
<?php if ($clinics === []): ?>
<div class="empty-state compact mt-4"><strong><?= qh_h(qh_t('No clinic records match this view.', 'لا توجد سجلات عيادات مطابقة لهذا العرض.')) ?></strong><span><?= qh_h(qh_t('Try another search or create a new clinic.', 'جرّب بحثاً آخر أو أنشئ عيادة جديدة.')) ?></span></div>
<?php else: ?>
<div class="table-responsive mt-4">
<table class="table align-middle admin-table">
<thead>
<tr>
<th><?= qh_h(qh_t('Code', 'الرمز')) ?></th>
<th><?= qh_h(qh_t('Clinic', 'العيادة')) ?></th>
<th><?= qh_h(qh_t('Routing', 'المسار')) ?></th>
<th><?= qh_h(qh_t('Order', 'الترتيب')) ?></th>
<th><?= qh_h(qh_t('Actions', 'الإجراءات')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($clinics as $clinic): ?>
<?php $editUrl = qh_url('admin_clinics.php', ['edit' => (int) $clinic['id'], 'q' => $search]); ?>
<tr>
<td><span class="room-badge"><?= qh_h((string) $clinic['code']) ?></span></td>
<td>
<div class="fw-semibold"><?= qh_h(qh_name($clinic)) ?></div>
<div class="small text-secondary"><?= qh_h(qh_is_ar() ? (string) ($clinic['name_en'] ?? '') : (string) ($clinic['name_ar'] ?? '')) ?></div>
</td>
<td><span class="table-pill <?= (int) $clinic['requires_vitals'] === 1 ? 'warning' : 'info' ?>"><?= qh_h((int) $clinic['requires_vitals'] === 1 ? qh_t('Vitals first', 'العلامات أولاً') : qh_t('Direct doctor', 'الطبيب مباشرة')) ?></span></td>
<td><?= qh_h((string) $clinic['sort_order']) ?></td>
<td>
<div class="admin-table-actions">
<a class="btn btn-sm btn-outline-dark" href="<?= qh_h($editUrl) ?>"><?= qh_h(qh_t('Edit', 'تعديل')) ?></a>
<form method="post" onsubmit="return confirm('<?= qh_h(qh_t('Delete this clinic record?', 'هل تريد حذف سجل هذه العيادة؟')) ?>');">
<input type="hidden" name="action" value="delete_clinic">
<input type="hidden" name="clinic_id" value="<?= qh_h((string) $clinic['id']) ?>">
<input type="hidden" name="return_to" value="admin_clinics.php">
<button class="btn btn-sm btn-outline-danger" type="submit"><?= qh_h(qh_t('Delete', 'حذف')) ?></button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</section>
<section class="panel-card admin-form-card">
<div class="admin-section-head">
<div>
<span class="section-kicker"><?= qh_h($editClinic ? qh_t('Edit clinic', 'تعديل العيادة') : qh_t('New clinic', 'عيادة جديدة')) ?></span>
<h2 class="section-title mt-2 mb-1"><?= qh_h($editClinic ? qh_t('Update clinic profile', 'تحديث ملف العيادة') : qh_t('Create clinic profile', 'إنشاء ملف العيادة')) ?></h2>
<p class="section-copy mb-0"><?= qh_h(qh_t('Define the code, both names, routing type, and display order in one focused form.', 'حدّد الرمز والاسمين ونوع المسار وترتيب العرض في نموذج واحد مركز.')) ?></p>
</div>
</div>
<form method="post" class="vstack gap-3 mt-4">
<input type="hidden" name="action" value="<?= qh_h($editClinic ? 'update_clinic' : 'add_clinic') ?>">
<input type="hidden" name="return_to" value="admin_clinics.php">
<?php if ($editClinic): ?>
<input type="hidden" name="clinic_id" value="<?= qh_h((string) $editClinic['id']) ?>">
<?php endif; ?>
<div class="row g-3">
<div class="col-sm-4">
<label class="form-label" for="clinicCode"><?= qh_h(qh_t('Clinic code', 'رمز العيادة')) ?></label>
<input id="clinicCode" class="form-control" type="text" maxlength="10" name="code" value="<?= qh_h((string) ($editClinic['code'] ?? '')) ?>" required>
</div>
<div class="col-sm-8">
<label class="form-label" for="clinicOrder"><?= qh_h(qh_t('Display order', 'ترتيب العرض')) ?></label>
<input id="clinicOrder" class="form-control" type="number" min="1" name="sort_order" value="<?= qh_h((string) ($editClinic['sort_order'] ?? 50)) ?>" required>
</div>
</div>
<div>
<label class="form-label" for="clinicNameEn"><?= qh_h(qh_t('Clinic name (English)', 'اسم العيادة بالإنجليزية')) ?></label>
<input id="clinicNameEn" class="form-control" type="text" name="name_en" value="<?= qh_h((string) ($editClinic['name_en'] ?? '')) ?>" required>
</div>
<div>
<label class="form-label" for="clinicNameAr"><?= qh_h(qh_t('Clinic name (Arabic)', 'اسم العيادة بالعربية')) ?></label>
<input id="clinicNameAr" class="form-control" type="text" name="name_ar" value="<?= qh_h((string) ($editClinic['name_ar'] ?? '')) ?>" required>
</div>
<div class="admin-switch-card">
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" role="switch" id="clinicVitals" name="requires_vitals" <?= (int) ($editClinic['requires_vitals'] ?? 0) === 1 ? 'checked' : '' ?>>
<label class="form-check-label" for="clinicVitals"><?= qh_h(qh_t('Require nursing vitals before doctor', 'يتطلب العلامات الحيوية في التمريض قبل الطبيب')) ?></label>
</div>
<div class="admin-form-note mt-2"><?= qh_h(qh_t('Turn this on for clinics that must stop at nursing before the doctor queue.', 'فعّل هذا الخيار للعيادات التي يجب أن تمر على التمريض قبل قائمة انتظار الطبيب.')) ?></div>
</div>
<div class="d-flex flex-wrap gap-2 pt-2">
<button class="btn btn-dark" type="submit"><?= qh_h($editClinic ? qh_t('Save changes', 'حفظ التعديلات') : qh_t('Add clinic', 'إضافة عيادة')) ?></button>
<?php if ($editClinic): ?>
<a class="btn btn-outline-dark" href="<?= qh_h(qh_url('admin_clinics.php')) ?>"><?= qh_h(qh_t('Cancel edit', 'إلغاء التعديل')) ?></a>
<?php endif; ?>
</div>
</form>
</section>
</div>
</div>
</div>
</div>
<?php qh_page_end(); ?>

196
admin_doctors.php Normal file
View File

@ -0,0 +1,196 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/queue_bootstrap.php';
qh_boot();
qh_admin_handle_request();
$stats = qh_admin_stats();
$clinics = qh_fetch_clinics();
$doctors = qh_fetch_doctors();
$search = trim((string) ($_GET['q'] ?? ''));
$editId = (int) ($_GET['edit'] ?? 0);
$editDoctor = $editId > 0 ? qh_fetch_doctor($editId) : null;
if ($editId > 0 && $editDoctor === null) {
qh_set_flash('warning', qh_t('The requested doctor record was not found.', 'لم يتم العثور على سجل الطبيب المطلوب.'));
qh_redirect('admin_doctors.php');
}
$toLower = static function (string $value): string {
return function_exists('mb_strtolower') ? mb_strtolower($value, 'UTF-8') : strtolower($value);
};
if ($search !== '') {
$needle = $toLower($search);
$doctors = array_values(array_filter($doctors, static function (array $doctor) use ($needle, $toLower): bool {
$haystack = implode(' ', [
(string) ($doctor['name_en'] ?? ''),
(string) ($doctor['name_ar'] ?? ''),
(string) ($doctor['clinic_name_en'] ?? ''),
(string) ($doctor['clinic_name_ar'] ?? ''),
(string) ($doctor['room_number'] ?? ''),
]);
return str_contains($toLower($haystack), $needle);
}));
}
qh_page_start(
'admin',
qh_t('Doctor management', 'إدارة الأطباء'),
qh_t('Professional doctor directory with search, edit, and delete actions.', 'دليل احترافي للأطباء مع البحث وخيارات التعديل والحذف.')
);
?>
<div class="container-xxl px-3 px-lg-4">
<div class="admin-layout">
<aside class="admin-sidebar-column">
<?php qh_render_admin_sidebar('admin_doctors.php', $stats); ?>
</aside>
<div class="admin-content-stack">
<section class="page-header-panel admin-hero-panel mb-0">
<div>
<span class="section-kicker"><?= qh_h(qh_t('Doctor directory', 'دليل الأطباء')) ?></span>
<div class="locale-chip mt-3"><?= qh_h(qh_current_language_badge()) ?></div>
<h1 class="section-title-xl mt-2 mb-2"><?= qh_h(qh_t('Dedicated doctor management with clear actions.', 'إدارة مستقلة للأطباء مع إجراءات واضحة.')) ?></h1>
<p class="section-copy mb-0"><?= qh_h(qh_t('Search doctors, edit assignments, and manage room records from a focused page.', 'ابحث عن الأطباء وعدّل التعيينات وأدر سجلات الغرف من صفحة مركزة.')) ?></p>
</div>
</section>
<section class="panel-card admin-toolbar-card">
<div class="admin-toolbar">
<form method="get" class="admin-search-form" role="search">
<input type="hidden" name="lang" value="<?= qh_h(qh_locale()) ?>">
<label class="visually-hidden" for="doctorSearch"><?= qh_h(qh_t('Search doctors', 'البحث عن الأطباء')) ?></label>
<input id="doctorSearch" class="form-control" type="search" name="q" value="<?= qh_h($search) ?>" placeholder="<?= qh_h(qh_t('Search by doctor, clinic, or room', 'ابحث باسم الطبيب أو العيادة أو الغرفة')) ?>">
<button class="btn btn-dark" type="submit"><?= qh_h(qh_t('Search', 'بحث')) ?></button>
<?php if ($search !== ''): ?>
<a class="btn btn-outline-dark" href="<?= qh_h(qh_url('admin_doctors.php')) ?>"><?= qh_h(qh_t('Reset', 'إعادة ضبط')) ?></a>
<?php endif; ?>
</form>
<?php if ($editDoctor !== null): ?>
<a class="btn btn-outline-dark" href="<?= qh_h(qh_url('admin_doctors.php')) ?>"><?= qh_h(qh_t('Add new doctor', 'إضافة طبيب جديد')) ?></a>
<?php endif; ?>
</div>
</section>
<div class="admin-directory-layout">
<section class="panel-card admin-table-card">
<div class="admin-section-head">
<div>
<span class="section-kicker"><?= qh_h(qh_t('Doctor list', 'قائمة الأطباء')) ?></span>
<h2 class="section-title mt-2 mb-1"><?= qh_h(qh_t('Doctors and room assignments', 'الأطباء وتعيينات الغرف')) ?></h2>
<p class="section-copy mb-0"><?= qh_h($search !== ''
? qh_t('Filtered results based on your search.', 'نتائج مفلترة بناءً على البحث.')
: qh_t('All configured doctor records.', 'جميع سجلات الأطباء المهيأة.')) ?></p>
</div>
<div class="admin-count-chip"><?= qh_h((string) count($doctors)) ?> <?= qh_h(qh_t('records', 'سجلات')) ?></div>
</div>
<?php if ($doctors === []): ?>
<div class="empty-state compact mt-4"><strong><?= qh_h(qh_t('No doctor records match this view.', 'لا توجد سجلات أطباء مطابقة لهذا العرض.')) ?></strong><span><?= qh_h(qh_t('Try another search or create a new doctor profile.', 'جرّب بحثاً آخر أو أنشئ ملف طبيب جديد.')) ?></span></div>
<?php else: ?>
<div class="table-responsive mt-4">
<table class="table align-middle admin-table">
<thead>
<tr>
<th><?= qh_h(qh_t('Doctor', 'الطبيب')) ?></th>
<th><?= qh_h(qh_t('Clinic', 'العيادة')) ?></th>
<th><?= qh_h(qh_t('Room', 'الغرفة')) ?></th>
<th><?= qh_h(qh_t('Order', 'الترتيب')) ?></th>
<th><?= qh_h(qh_t('Actions', 'الإجراءات')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($doctors as $doctor): ?>
<?php $editUrl = qh_url('admin_doctors.php', ['edit' => (int) $doctor['id'], 'q' => $search]); ?>
<tr>
<td>
<div class="fw-semibold"><?= qh_h(qh_name($doctor)) ?></div>
<div class="small text-secondary"><?= qh_h(qh_is_ar() ? (string) ($doctor['name_en'] ?? '') : (string) ($doctor['name_ar'] ?? '')) ?></div>
</td>
<td><?= qh_h(qh_name($doctor, 'clinic_name', qh_t('Unassigned', 'غير محدد'))) ?></td>
<td><span class="room-badge"><?= qh_h((string) $doctor['room_number']) ?></span></td>
<td><?= qh_h((string) $doctor['sort_order']) ?></td>
<td>
<div class="admin-table-actions">
<a class="btn btn-sm btn-outline-dark" href="<?= qh_h($editUrl) ?>"><?= qh_h(qh_t('Edit', 'تعديل')) ?></a>
<form method="post" onsubmit="return confirm('<?= qh_h(qh_t('Delete this doctor record?', 'هل تريد حذف سجل هذا الطبيب؟')) ?>');">
<input type="hidden" name="action" value="delete_doctor">
<input type="hidden" name="doctor_id" value="<?= qh_h((string) $doctor['id']) ?>">
<input type="hidden" name="return_to" value="admin_doctors.php">
<button class="btn btn-sm btn-outline-danger" type="submit"><?= qh_h(qh_t('Delete', 'حذف')) ?></button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</section>
<section class="panel-card admin-form-card">
<div class="admin-section-head">
<div>
<span class="section-kicker"><?= qh_h($editDoctor ? qh_t('Edit doctor', 'تعديل الطبيب') : qh_t('New doctor', 'طبيب جديد')) ?></span>
<h2 class="section-title mt-2 mb-1"><?= qh_h($editDoctor ? qh_t('Update doctor profile', 'تحديث ملف الطبيب') : qh_t('Create doctor profile', 'إنشاء ملف الطبيب')) ?></h2>
<p class="section-copy mb-0"><?= qh_h(qh_t('Store both English and Arabic names while keeping the page language separate.', 'احفظ الاسمين الإنجليزي والعربي مع إبقاء لغة الصفحة منفصلة.')) ?></p>
</div>
</div>
<?php if ($clinics === []): ?>
<div class="empty-state compact mt-4"><strong><?= qh_h(qh_t('Clinics are required first.', 'يجب إعداد العيادات أولاً.')) ?></strong><span><?= qh_h(qh_t('Create at least one clinic before assigning doctors.', 'أنشئ عيادة واحدة على الأقل قبل تعيين الأطباء.')) ?></span></div>
<a class="btn btn-dark mt-3" href="<?= qh_h(qh_url('admin_clinics.php')) ?>"><?= qh_h(qh_t('Open clinics page', 'فتح صفحة العيادات')) ?></a>
<?php else: ?>
<form method="post" class="vstack gap-3 mt-4">
<input type="hidden" name="action" value="<?= qh_h($editDoctor ? 'update_doctor' : 'add_doctor') ?>">
<input type="hidden" name="return_to" value="admin_doctors.php">
<?php if ($editDoctor): ?>
<input type="hidden" name="doctor_id" value="<?= qh_h((string) $editDoctor['id']) ?>">
<?php endif; ?>
<div>
<label class="form-label" for="doctorNameEn"><?= qh_h(qh_t('Doctor name (English)', 'اسم الطبيب بالإنجليزية')) ?></label>
<input id="doctorNameEn" class="form-control" type="text" name="name_en" value="<?= qh_h((string) ($editDoctor['name_en'] ?? '')) ?>" required>
</div>
<div>
<label class="form-label" for="doctorNameAr"><?= qh_h(qh_t('Doctor name (Arabic)', 'اسم الطبيب بالعربية')) ?></label>
<input id="doctorNameAr" class="form-control" type="text" name="name_ar" value="<?= qh_h((string) ($editDoctor['name_ar'] ?? '')) ?>" required>
</div>
<div>
<label class="form-label" for="doctorClinic"><?= qh_h(qh_t('Clinic', 'العيادة')) ?></label>
<select id="doctorClinic" class="form-select" name="clinic_id" required>
<?php foreach ($clinics as $clinic): ?>
<option value="<?= qh_h((string) $clinic['id']) ?>" <?= (int) ($editDoctor['clinic_id'] ?? 0) === (int) $clinic['id'] ? 'selected' : '' ?>><?= qh_h(qh_name($clinic)) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="row g-3">
<div class="col-sm-6">
<label class="form-label" for="doctorRoom"><?= qh_h(qh_t('Room number', 'رقم الغرفة')) ?></label>
<input id="doctorRoom" class="form-control" type="text" name="room_number" value="<?= qh_h((string) ($editDoctor['room_number'] ?? '')) ?>" required>
</div>
<div class="col-sm-6">
<label class="form-label" for="doctorOrder"><?= qh_h(qh_t('Display order', 'ترتيب العرض')) ?></label>
<input id="doctorOrder" class="form-control" type="number" min="1" name="sort_order" value="<?= qh_h((string) ($editDoctor['sort_order'] ?? 50)) ?>" required>
</div>
</div>
<div class="d-flex flex-wrap gap-2 pt-2">
<button class="btn btn-dark" type="submit"><?= qh_h($editDoctor ? qh_t('Save changes', 'حفظ التعديلات') : qh_t('Add doctor', 'إضافة طبيب')) ?></button>
<?php if ($editDoctor): ?>
<a class="btn btn-outline-dark" href="<?= qh_h(qh_url('admin_doctors.php')) ?>"><?= qh_h(qh_t('Cancel edit', 'إلغاء التعديل')) ?></a>
<?php endif; ?>
</div>
</form>
<?php endif; ?>
</section>
</div>
</div>
</div>
</div>
<?php qh_page_end(); ?>

243
admin_hospital.php Normal file
View File

@ -0,0 +1,243 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/queue_bootstrap.php';
qh_boot();
qh_admin_handle_request();
$stats = qh_admin_stats();
$profile = qh_fetch_hospital_profile();
$logoUrl = qh_hospital_logo_url();
$faviconUrl = qh_hospital_favicon_url();
$tagline = qh_hospital_tagline();
$website = trim((string) ($profile['website'] ?? ''));
$websiteIsValid = $website !== '' && filter_var($website, FILTER_VALIDATE_URL) !== false;
qh_page_start(
'admin',
qh_t('Hospital profile', 'ملف المستشفى'),
qh_t('Manage the hospital brand, logo, favicon, contact details, and presentation settings.', 'إدارة هوية المستشفى والشعار والأيقونة وبيانات التواصل وإعدادات العرض.')
);
?>
<div class="container-xxl px-3 px-lg-4">
<div class="admin-layout">
<aside class="admin-sidebar-column">
<?php qh_render_admin_sidebar('admin_hospital.php', $stats); ?>
</aside>
<div class="admin-content-stack">
<section class="page-header-panel admin-hero-panel mb-0 hospital-hero-panel">
<div>
<span class="section-kicker"><?= qh_h(qh_t('Brand settings', 'إعدادات الهوية')) ?></span>
<div class="locale-chip mt-3"><?= qh_h(qh_current_language_badge()) ?></div>
<h1 class="section-title-xl mt-2 mb-2"><?= qh_h(qh_t('Hospital profile and branding.', 'ملف المستشفى والهوية البصرية.')) ?></h1>
<p class="section-copy mb-0"><?= qh_h(qh_t('Set the hospital name, logo, favicon, contact details, working hours, and brand colors from one dedicated page.', 'اضبط اسم المستشفى والشعار والأيقونة وبيانات التواصل وساعات العمل وألوان الهوية من صفحة مخصصة واحدة.')) ?></p>
</div>
<div class="hospital-brand-preview">
<div class="hospital-brand-mark">
<?php if ($logoUrl !== ''): ?>
<img src="<?= qh_h($logoUrl) ?>" alt="<?= qh_h(qh_hospital_name()) ?>">
<?php else: ?>
<span><?= qh_h(qh_hospital_brand_initials()) ?></span>
<?php endif; ?>
</div>
<div>
<div class="section-kicker mb-2"><?= qh_h(qh_t('Live preview', 'معاينة مباشرة')) ?></div>
<h2 class="section-title mb-1"><?= qh_h(qh_hospital_name()) ?></h2>
<?php if ($tagline !== ''): ?>
<p class="section-copy mb-2"><?= qh_h($tagline) ?></p>
<?php endif; ?>
<div class="hospital-color-pills">
<span class="hospital-color-pill"><span class="hospital-color-swatch" style="background: <?= qh_h(qh_hospital_primary_color()) ?>"></span><?= qh_h(qh_hospital_primary_color()) ?></span>
<span class="hospital-color-pill"><span class="hospital-color-swatch" style="background: <?= qh_h(qh_hospital_secondary_color()) ?>"></span><?= qh_h(qh_hospital_secondary_color()) ?></span>
</div>
</div>
</div>
</section>
<div class="admin-directory-layout hospital-profile-layout">
<section class="panel-card admin-form-card">
<div class="admin-section-head">
<div>
<span class="section-kicker"><?= qh_h(qh_t('Profile editor', 'تحرير الملف')) ?></span>
<h2 class="section-title mt-2 mb-1"><?= qh_h(qh_t('Update hospital details', 'تحديث بيانات المستشفى')) ?></h2>
<p class="section-copy mb-0"><?= qh_h(qh_t('Use image URLs for the logo and favicon. The saved branding is reused in the top navigation and browser tab.', 'استخدم روابط صور للشعار والأيقونة. سيتم استخدام الهوية المحفوظة في شريط التنقل العلوي وتبويب المتصفح.')) ?></p>
</div>
</div>
<form method="post" class="vstack gap-4 mt-4">
<input type="hidden" name="action" value="save_hospital_profile">
<input type="hidden" name="return_to" value="admin_hospital.php">
<div class="row g-3">
<div class="col-lg-6">
<label class="form-label" for="hospitalNameEn"><?= qh_h(qh_t('Hospital name (English)', 'اسم المستشفى بالإنجليزية')) ?></label>
<input id="hospitalNameEn" class="form-control" type="text" name="name_en" value="<?= qh_h((string) ($profile['name_en'] ?? '')) ?>" required>
</div>
<div class="col-lg-6">
<label class="form-label" for="hospitalNameAr"><?= qh_h(qh_t('Hospital name (Arabic)', 'اسم المستشفى بالعربية')) ?></label>
<input id="hospitalNameAr" class="form-control" type="text" name="name_ar" value="<?= qh_h((string) ($profile['name_ar'] ?? '')) ?>" required>
</div>
</div>
<div class="row g-3">
<div class="col-lg-4">
<label class="form-label" for="hospitalShortName"><?= qh_h(qh_t('Short name', 'الاسم المختصر')) ?></label>
<input id="hospitalShortName" class="form-control" type="text" maxlength="40" name="short_name" value="<?= qh_h((string) ($profile['short_name'] ?? '')) ?>">
</div>
<div class="col-lg-4">
<label class="form-label" for="hospitalPhone"><?= qh_h(qh_t('Phone number', 'رقم الهاتف')) ?></label>
<input id="hospitalPhone" class="form-control" type="text" name="phone" value="<?= qh_h((string) ($profile['phone'] ?? '')) ?>">
</div>
<div class="col-lg-4">
<label class="form-label" for="hospitalEmail"><?= qh_h(qh_t('Email address', 'البريد الإلكتروني')) ?></label>
<input id="hospitalEmail" class="form-control" type="email" name="email" value="<?= qh_h((string) ($profile['email'] ?? '')) ?>">
</div>
</div>
<div class="row g-3">
<div class="col-lg-6">
<label class="form-label" for="hospitalTaglineEn"><?= qh_h(qh_t('Tagline (English)', 'الشعار النصي بالإنجليزية')) ?></label>
<input id="hospitalTaglineEn" class="form-control" type="text" name="tagline_en" value="<?= qh_h((string) ($profile['tagline_en'] ?? '')) ?>">
</div>
<div class="col-lg-6">
<label class="form-label" for="hospitalTaglineAr"><?= qh_h(qh_t('Tagline (Arabic)', 'الشعار النصي بالعربية')) ?></label>
<input id="hospitalTaglineAr" class="form-control" type="text" name="tagline_ar" value="<?= qh_h((string) ($profile['tagline_ar'] ?? '')) ?>">
</div>
</div>
<div class="row g-3">
<div class="col-lg-6">
<label class="form-label" for="hospitalAddressEn"><?= qh_h(qh_t('Address (English)', 'العنوان بالإنجليزية')) ?></label>
<textarea id="hospitalAddressEn" class="form-control" name="address_en" rows="3"><?= qh_h((string) ($profile['address_en'] ?? '')) ?></textarea>
</div>
<div class="col-lg-6">
<label class="form-label" for="hospitalAddressAr"><?= qh_h(qh_t('Address (Arabic)', 'العنوان بالعربية')) ?></label>
<textarea id="hospitalAddressAr" class="form-control" name="address_ar" rows="3"><?= qh_h((string) ($profile['address_ar'] ?? '')) ?></textarea>
</div>
</div>
<div class="row g-3">
<div class="col-lg-6">
<label class="form-label" for="hospitalHoursEn"><?= qh_h(qh_t('Working hours (English)', 'ساعات العمل بالإنجليزية')) ?></label>
<input id="hospitalHoursEn" class="form-control" type="text" name="working_hours_en" value="<?= qh_h((string) ($profile['working_hours_en'] ?? '')) ?>">
</div>
<div class="col-lg-6">
<label class="form-label" for="hospitalHoursAr"><?= qh_h(qh_t('Working hours (Arabic)', 'ساعات العمل بالعربية')) ?></label>
<input id="hospitalHoursAr" class="form-control" type="text" name="working_hours_ar" value="<?= qh_h((string) ($profile['working_hours_ar'] ?? '')) ?>">
</div>
</div>
<div class="row g-3">
<div class="col-lg-6">
<label class="form-label" for="hospitalWebsite"><?= qh_h(qh_t('Website URL', 'رابط الموقع الإلكتروني')) ?></label>
<input id="hospitalWebsite" class="form-control" type="url" name="website" placeholder="https://example.com" value="<?= qh_h((string) ($profile['website'] ?? '')) ?>">
</div>
<div class="col-lg-6">
<label class="form-label" for="hospitalLogoUrl"><?= qh_h(qh_t('Logo image URL', 'رابط صورة الشعار')) ?></label>
<input id="hospitalLogoUrl" class="form-control" type="url" name="logo_url" placeholder="https://.../logo.png" value="<?= qh_h((string) ($profile['logo_url'] ?? '')) ?>">
</div>
</div>
<div class="row g-3">
<div class="col-lg-6">
<label class="form-label" for="hospitalFaviconUrl"><?= qh_h(qh_t('Favicon URL', 'رابط الأيقونة')) ?></label>
<input id="hospitalFaviconUrl" class="form-control" type="url" name="favicon_url" placeholder="https://.../favicon.png" value="<?= qh_h((string) ($profile['favicon_url'] ?? '')) ?>">
</div>
<div class="col-lg-3">
<label class="form-label" for="hospitalPrimaryColor"><?= qh_h(qh_t('Primary color', 'اللون الأساسي')) ?></label>
<input id="hospitalPrimaryColor" class="form-control form-control-color w-100" type="color" name="primary_color" value="<?= qh_h(qh_hospital_primary_color()) ?>">
</div>
<div class="col-lg-3">
<label class="form-label" for="hospitalSecondaryColor"><?= qh_h(qh_t('Secondary color', 'اللون الثانوي')) ?></label>
<input id="hospitalSecondaryColor" class="form-control form-control-color w-100" type="color" name="secondary_color" value="<?= qh_h(qh_hospital_secondary_color()) ?>">
</div>
</div>
<div class="d-flex flex-wrap gap-2 pt-2">
<button class="btn btn-dark" type="submit"><?= qh_h(qh_t('Save hospital profile', 'حفظ ملف المستشفى')) ?></button>
<a class="btn btn-outline-dark" href="<?= qh_h(qh_url('admin.php')) ?>"><?= qh_h(qh_t('Back to overview', 'العودة إلى النظرة العامة')) ?></a>
</div>
</form>
</section>
<div class="vstack gap-4">
<section class="panel-card hospital-preview-card">
<div class="admin-section-head">
<div>
<span class="section-kicker"><?= qh_h(qh_t('Brand preview', 'معاينة الهوية')) ?></span>
<h2 class="section-title mt-2 mb-1"><?= qh_h(qh_t('What staff will see', 'ما الذي سيراه الطاقم')) ?></h2>
<p class="section-copy mb-0"><?= qh_h(qh_t('This preview mirrors the saved logo, brand colors, and hospital details used by the app shell.', 'تعكس هذه المعاينة الشعار والألوان وبيانات المستشفى المستخدمة في واجهة التطبيق.')) ?></p>
</div>
</div>
<div class="hospital-preview-stack mt-4">
<div class="hospital-preview-header">
<div class="hospital-preview-logo">
<?php if ($logoUrl !== ''): ?>
<img src="<?= qh_h($logoUrl) ?>" alt="<?= qh_h(qh_hospital_name()) ?>">
<?php else: ?>
<span><?= qh_h(qh_hospital_brand_initials()) ?></span>
<?php endif; ?>
</div>
<div>
<strong><?= qh_h(qh_hospital_name()) ?></strong>
<?php if ($tagline !== ''): ?>
<p class="section-copy mb-0"><?= qh_h($tagline) ?></p>
<?php endif; ?>
</div>
</div>
<dl class="hospital-detail-list mb-0">
<?php if (qh_hospital_contact_value('phone') !== ''): ?>
<div><dt><?= qh_h(qh_t('Phone', 'الهاتف')) ?></dt><dd><?= qh_h(qh_hospital_contact_value('phone')) ?></dd></div>
<?php endif; ?>
<?php if (qh_hospital_contact_value('email') !== ''): ?>
<div><dt><?= qh_h(qh_t('Email', 'البريد الإلكتروني')) ?></dt><dd><?= qh_h(qh_hospital_contact_value('email')) ?></dd></div>
<?php endif; ?>
<?php if ($websiteIsValid): ?>
<div><dt><?= qh_h(qh_t('Website', 'الموقع الإلكتروني')) ?></dt><dd><a href="<?= qh_h($website) ?>" target="_blank" rel="noreferrer"><?= qh_h($website) ?></a></dd></div>
<?php endif; ?>
<?php if (qh_hospital_contact_value(qh_is_ar() ? 'address_ar' : 'address_en') !== ''): ?>
<div><dt><?= qh_h(qh_t('Address', 'العنوان')) ?></dt><dd><?= nl2br(qh_h(qh_hospital_contact_value(qh_is_ar() ? 'address_ar' : 'address_en'))) ?></dd></div>
<?php endif; ?>
<?php if (qh_hospital_contact_value(qh_is_ar() ? 'working_hours_ar' : 'working_hours_en') !== ''): ?>
<div><dt><?= qh_h(qh_t('Working hours', 'ساعات العمل')) ?></dt><dd><?= qh_h(qh_hospital_contact_value(qh_is_ar() ? 'working_hours_ar' : 'working_hours_en')) ?></dd></div>
<?php endif; ?>
</dl>
</div>
</section>
<section class="panel-card hospital-preview-card">
<div class="admin-section-head">
<div>
<span class="section-kicker"><?= qh_h(qh_t('Assets', 'الأصول')) ?></span>
<h2 class="section-title mt-2 mb-1"><?= qh_h(qh_t('Logo and favicon status', 'حالة الشعار والأيقونة')) ?></h2>
</div>
</div>
<div class="hospital-asset-grid mt-4">
<div class="hospital-asset-box">
<span class="workflow-step"><?= qh_h(qh_t('Logo', 'الشعار')) ?></span>
<?php if ($logoUrl !== ''): ?>
<img class="hospital-asset-image" src="<?= qh_h($logoUrl) ?>" alt="<?= qh_h(qh_t('Hospital logo preview', 'معاينة شعار المستشفى')) ?>">
<?php else: ?>
<div class="empty-state compact mt-3"><strong><?= qh_h(qh_t('No logo yet.', 'لا يوجد شعار حتى الآن.')) ?></strong><span><?= qh_h(qh_t('Add a logo URL to replace the initials badge in the top header.', 'أضف رابط شعار لاستبدال شارة الأحرف في الترويسة العلوية.')) ?></span></div>
<?php endif; ?>
</div>
<div class="hospital-asset-box">
<span class="workflow-step"><?= qh_h(qh_t('Favicon', 'الأيقونة')) ?></span>
<?php if ($faviconUrl !== ''): ?>
<img class="hospital-asset-image hospital-asset-favicon" src="<?= qh_h($faviconUrl) ?>" alt="<?= qh_h(qh_t('Favicon preview', 'معاينة الأيقونة')) ?>">
<?php else: ?>
<div class="empty-state compact mt-3"><strong><?= qh_h(qh_t('No custom favicon yet.', 'لا توجد أيقونة مخصصة حتى الآن.')) ?></strong><span><?= qh_h(qh_t('If left empty, the app will reuse the logo as the browser icon when available.', 'إذا تُرك الحقل فارغاً، سيعيد التطبيق استخدام الشعار كأيقونة للمتصفح عند توفره.')) ?></span></div>
<?php endif; ?>
</div>
</div>
</section>
</div>
</div>
</div>
</div>
</div>
<?php qh_page_end(); ?>

View File

@ -1,18 +1,20 @@
:root { :root {
--bg: #f3f4f6; --bg: #eef6f7;
--surface: #ffffff; --surface: #ffffff;
--surface-muted: #f8fafc; --surface-muted: #f7fbfc;
--border: #d7dde6; --border: #d5e3e8;
--border-strong: #c4ccd7; --border-strong: #b7ccd5;
--text: #111827; --text: #183b4d;
--muted: #6b7280; --ink: #183b4d;
--accent: #1f4f78; --muted: #61788a;
--accent-soft: #e8f0f6; --accent: #0f8b8d;
--accent-strong: #16697a;
--accent-soft: #e1f4f3;
--warning-soft: #fff4d6; --warning-soft: #fff4d6;
--info-soft: #dceef8; --info-soft: #dceef8;
--success-soft: #dff3e7; --success-soft: #dff3e7;
--danger-soft: #fde4e4; --danger-soft: #fde4e4;
--shadow: 0 10px 30px rgba(15, 23, 42, 0.06); --shadow: 0 18px 40px rgba(15, 64, 75, 0.08);
--radius-sm: 0.45rem; --radius-sm: 0.45rem;
--radius-md: 0.7rem; --radius-md: 0.7rem;
--radius-lg: 0.95rem; --radius-lg: 0.95rem;
@ -54,6 +56,11 @@ a:hover {
text-decoration: none; text-decoration: none;
} }
.app-header {
background: rgba(255, 255, 255, 0.88);
backdrop-filter: blur(14px);
}
.brand-mark { .brand-mark {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -61,11 +68,12 @@ a:hover {
width: 2.25rem; width: 2.25rem;
height: 2.25rem; height: 2.25rem;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
background: var(--text); background: linear-gradient(135deg, var(--accent-strong) 0%, var(--accent) 100%);
color: #fff; color: #fff;
font-size: 0.88rem; font-size: 0.88rem;
font-weight: 700; font-weight: 700;
letter-spacing: 0.04em; letter-spacing: 0.04em;
box-shadow: 0 12px 20px rgba(15, 139, 141, 0.22);
} }
.brand-text { .brand-text {
@ -73,6 +81,36 @@ a:hover {
font-weight: 700; font-weight: 700;
} }
.brand-copy {
display: inline-flex;
flex-direction: column;
gap: 0.05rem;
min-width: 0;
}
.brand-subtext {
color: var(--muted);
font-size: 0.78rem;
line-height: 1.2;
max-width: 34ch;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.brand-mark-image {
overflow: hidden;
padding: 0;
background: #ffffff;
}
.brand-mark-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.nav-link { .nav-link {
color: var(--muted); color: var(--muted);
font-weight: 500; font-weight: 500;
@ -82,8 +120,8 @@ a:hover {
.nav-link:hover, .nav-link:hover,
.nav-link.active { .nav-link.active {
background: #eef2f6; background: var(--accent-soft);
color: var(--text); color: var(--accent-strong);
} }
.app-shell { .app-shell {
@ -491,3 +529,633 @@ a:hover {
font-size: 2.5rem; font-size: 2.5rem;
} }
} }
.admin-layout {
display: grid;
grid-template-columns: minmax(220px, 248px) minmax(0, 1fr);
gap: 1.25rem;
align-items: start;
}
.admin-sidebar-column {
min-width: 0;
}
.admin-sidebar {
position: sticky;
top: 5.9rem;
display: grid;
gap: 1rem;
}
.admin-sidebar-top {
display: grid;
gap: 0.35rem;
}
.admin-sidebar-nav {
display: grid;
gap: 0.3rem;
padding: 0.35rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: linear-gradient(180deg, #ffffff 0%, #f8fbfc 100%);
}
.admin-sidebar-link {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 0.7rem;
padding: 0.72rem 0.8rem;
border: 1px solid transparent;
border-radius: 0.95rem;
background: transparent;
color: var(--muted);
text-decoration: none;
font-weight: 700;
line-height: 1.2;
transition: color 0.18s ease, border-color 0.18s ease, background 0.18s ease;
}
.admin-sidebar-link:hover {
color: var(--accent-strong);
border-color: rgba(15, 139, 141, 0.14);
background: rgba(15, 139, 141, 0.08);
}
.admin-sidebar-link-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 0.75rem;
border: 1px solid var(--border);
background: var(--surface-muted);
color: var(--accent-strong);
flex-shrink: 0;
}
.admin-sidebar-link-icon svg {
width: 1rem;
height: 1rem;
}
.admin-sidebar-link-text {
display: inline-flex;
align-items: center;
min-width: 0;
}
.admin-sidebar-meta {
display: grid;
gap: 0.75rem;
}
.admin-mini-stat {
padding: 0.95rem 1rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
}
.admin-mini-stat-value {
display: block;
font-size: 1.4rem;
font-weight: 800;
line-height: 1.1;
}
.admin-mini-stat-label {
display: block;
margin-top: 0.3rem;
color: var(--muted);
font-size: 0.85rem;
}
.admin-content-stack {
display: grid;
gap: 1.25rem;
min-width: 0;
}
.admin-hero-panel {
background: linear-gradient(135deg, #ffffff 0%, #eef4f9 100%);
}
.admin-section-card {
scroll-margin-top: 6.75rem;
}
.admin-section-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}
.admin-overview-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1rem;
}
.admin-overview-card {
border: 1px solid var(--border);
border-radius: var(--radius-md);
}
.admin-overview-card strong {
display: block;
margin-top: 0.9rem;
font-size: clamp(1.8rem, 3vw, 2.35rem);
line-height: 1;
}
.admin-overview-card p {
margin-top: 0.65rem;
}
.admin-switch-card {
padding: 0.9rem 1rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface-muted);
}
.admin-form-note {
color: var(--muted);
font-size: 0.92rem;
}
.admin-list-form {
display: grid;
gap: 0.4rem;
}
.admin-list-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.admin-inline-switch {
padding: 0.65rem 0.85rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: #ffffff;
}
@media (max-width: 1199.98px) {
.admin-overview-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 991.98px) {
.admin-layout {
grid-template-columns: 1fr;
}
.admin-sidebar {
position: static;
}
}
@media (max-width: 767.98px) {
.admin-overview-grid {
grid-template-columns: 1fr;
}
.admin-list-head {
flex-direction: column;
align-items: flex-start;
}
.admin-sidebar-link {
padding-inline: 0.75rem;
}
}
/* language split updates */
.lang-switcher {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.lang-switch-link {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.45rem 0.85rem;
border-radius: 999px;
border: 1px solid var(--border-strong);
background: rgba(255, 255, 255, 0.7);
color: var(--ink);
font-size: 0.84rem;
font-weight: 700;
text-decoration: none;
}
.lang-switch-link.active {
background: var(--ink);
border-color: var(--ink);
color: #fff;
}
.locale-chip {
display: inline-flex;
align-items: center;
padding: 0.4rem 0.8rem;
border-radius: 999px;
background: rgba(17, 24, 39, 0.08);
color: var(--ink);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
html[dir="rtl"] body {
direction: rtl;
}
html[dir="rtl"] .section-title,
html[dir="rtl"] .section-title-xl,
html[dir="rtl"] .section-copy,
html[dir="rtl"] .display-title,
html[dir="rtl"] .form-label,
html[dir="rtl"] th,
html[dir="rtl"] td,
html[dir="rtl"] .navbar-brand,
html[dir="rtl"] .admin-sidebar,
html[dir="rtl"] .empty-state,
html[dir="rtl"] .mini-overview-card,
html[dir="rtl"] .ticket-card,
html[dir="rtl"] .timeline-item,
html[dir="rtl"] .detail-list {
text-align: right;
}
html[dir="rtl"] .navbar-nav {
padding-right: 0;
}
html[dir="rtl"] .toast-container {
left: 0;
right: auto;
}
html[dir="rtl"] .admin-list-head,
html[dir="rtl"] .call-strip,
html[dir="rtl"] .announcement-card,
html[dir="rtl"] .admin-sidebar-link {
flex-direction: row-reverse;
}
html[dir="rtl"] .timeline-item {
grid-template-columns: 1fr 18px;
}
html[dir="rtl"] .timeline-dot {
justify-self: center;
}
.btn-dark {
background: linear-gradient(135deg, var(--accent-strong) 0%, var(--accent) 100%);
border-color: var(--accent-strong);
}
.btn-dark:hover,
.btn-dark:focus {
background: linear-gradient(135deg, #135464 0%, #0b7f81 100%);
border-color: #135464;
}
.btn-outline-dark {
color: var(--accent-strong);
border-color: rgba(22, 105, 122, 0.28);
}
.btn-outline-dark:hover,
.btn-outline-dark:focus,
.btn-outline-dark:active {
color: #fff;
background: var(--accent-strong);
border-color: var(--accent-strong);
}
.admin-sidebar-link.active {
color: var(--accent-strong);
border-color: rgba(15, 139, 141, 0.24);
background: linear-gradient(135deg, #ffffff 0%, #ebf8f8 100%);
box-shadow: inset 0 0 0 1px rgba(15, 139, 141, 0.08);
}
.admin-sidebar-link.active .admin-sidebar-link-icon {
border-color: rgba(15, 139, 141, 0.18);
background: rgba(15, 139, 141, 0.12);
}
.admin-card-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1.25rem;
}
.admin-card-grid-triple {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.admin-card-grid-secondary {
align-items: start;
}
.admin-link-card {
display: flex;
flex-direction: column;
min-height: 100%;
}
.admin-summary-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.95rem 1rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface-muted);
}
.admin-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.admin-search-form {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1 1 32rem;
flex-wrap: wrap;
}
.admin-search-form .form-control {
flex: 1 1 16rem;
}
.admin-directory-layout {
display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(320px, 0.95fr);
gap: 1.25rem;
align-items: start;
}
.admin-table-card,
.admin-form-card {
min-width: 0;
}
.admin-table-actions {
display: flex;
align-items: center;
gap: 0.55rem;
flex-wrap: wrap;
}
.admin-table-actions form {
margin: 0;
}
.admin-count-chip {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 0.85rem;
border-radius: 999px;
background: var(--accent-soft);
color: var(--accent-strong);
font-size: 0.82rem;
font-weight: 700;
}
.admin-table tbody tr:hover {
background: rgba(15, 139, 141, 0.04);
}
@media (max-width: 1199.98px) {
.admin-card-grid,
.admin-directory-layout {
grid-template-columns: 1fr;
}
}
@media (max-width: 767.98px) {
.admin-summary-row,
.admin-toolbar,
.admin-search-form,
.admin-table-actions {
flex-direction: column;
align-items: stretch;
}
}
html[dir="rtl"] .admin-summary-row,
html[dir="rtl"] .admin-toolbar,
html[dir="rtl"] .admin-table-actions {
flex-direction: row-reverse;
}
.hospital-hero-panel {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
flex-wrap: wrap;
}
.hospital-brand-preview {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.1rem;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(225,244,243,0.9) 100%);
min-width: min(100%, 24rem);
}
.hospital-brand-mark,
.hospital-preview-logo {
width: 4.5rem;
height: 4.5rem;
border-radius: 1.1rem;
background: linear-gradient(135deg, var(--accent-strong) 0%, var(--accent) 100%);
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
font-weight: 800;
box-shadow: 0 12px 26px rgba(15, 139, 141, 0.22);
overflow: hidden;
}
.hospital-brand-mark img,
.hospital-preview-logo img,
.hospital-asset-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.hospital-color-pills {
display: flex;
align-items: center;
gap: 0.55rem;
flex-wrap: wrap;
}
.hospital-color-pill {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.75rem;
border-radius: 999px;
border: 1px solid var(--border);
background: var(--surface);
font-size: 0.82rem;
font-weight: 700;
color: var(--accent-strong);
}
.hospital-color-swatch {
width: 0.9rem;
height: 0.9rem;
border-radius: 999px;
border: 1px solid rgba(0, 0, 0, 0.08);
}
.hospital-profile-layout {
grid-template-columns: minmax(0, 1.45fr) minmax(300px, 0.95fr);
}
.hospital-preview-card,
.hospital-overview-card {
min-width: 0;
}
.hospital-preview-stack {
display: grid;
gap: 1rem;
}
.hospital-preview-header {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
border-radius: var(--radius-md);
border: 1px solid var(--border);
background: var(--surface-muted);
}
.hospital-detail-list {
display: grid;
gap: 0.85rem;
}
.hospital-detail-list div {
padding: 0.9rem 1rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: #fff;
}
.hospital-detail-list dt {
margin-bottom: 0.25rem;
color: var(--muted);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.hospital-detail-list dd {
margin: 0;
color: var(--text);
}
.hospital-asset-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.hospital-asset-box {
padding: 1rem;
border-radius: var(--radius-md);
border: 1px solid var(--border);
background: var(--surface-muted);
}
.hospital-asset-image {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
margin-top: 0.9rem;
border-radius: var(--radius-md);
border: 1px solid var(--border);
background: #fff;
}
.hospital-asset-favicon {
max-width: 6rem;
aspect-ratio: 1;
}
@media (max-width: 1199.98px) {
.admin-card-grid-triple,
.hospital-profile-layout {
grid-template-columns: 1fr;
}
}
@media (max-width: 767.98px) {
.hospital-brand-preview,
.hospital-preview-header {
flex-direction: column;
align-items: flex-start;
}
.hospital-asset-grid {
grid-template-columns: 1fr;
}
.brand-subtext {
white-space: normal;
}
}
html[dir="rtl"] .hospital-brand-preview,
html[dir="rtl"] .hospital-preview-header {
flex-direction: row-reverse;
}
html[dir="rtl"] .hospital-detail-list dt,
html[dir="rtl"] .hospital-detail-list dd {
text-align: right;
}

View File

@ -36,23 +36,25 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
const locale = document.body.dataset.locale || 'en';
const liveClock = document.querySelector('.js-live-clock'); const liveClock = document.querySelector('.js-live-clock');
if (liveClock) { if (liveClock) {
const renderClock = () => { const renderClock = () => {
const now = new Date(); const now = new Date();
liveClock.textContent = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); liveClock.textContent = now.toLocaleTimeString(locale === 'ar' ? 'ar-SA' : 'en-US', { hour: '2-digit', minute: '2-digit' });
}; };
renderClock(); renderClock();
window.setInterval(renderClock, 1000 * 30); window.setInterval(renderClock, 1000 * 30);
} }
const page = document.body.dataset.page; if (document.body.dataset.page === 'display') {
if (page === 'display') {
const fullscreenButton = document.querySelector('.js-fullscreen-toggle'); const fullscreenButton = document.querySelector('.js-fullscreen-toggle');
const syncFullscreenButton = () => { const syncFullscreenButton = () => {
if (!fullscreenButton) return; if (!fullscreenButton) return;
const isFullscreen = !!document.fullscreenElement; const isFullscreen = !!document.fullscreenElement;
fullscreenButton.textContent = isFullscreen ? 'Exit full display / إنهاء العرض الكامل' : 'Full display / عرض كامل'; fullscreenButton.textContent = isFullscreen
? (fullscreenButton.dataset.labelExit || 'Exit full display')
: (fullscreenButton.dataset.labelEnter || 'Full display');
fullscreenButton.setAttribute('aria-pressed', isFullscreen ? 'true' : 'false'); fullscreenButton.setAttribute('aria-pressed', isFullscreen ? 'true' : 'false');
}; };
@ -78,22 +80,20 @@ document.addEventListener('DOMContentLoaded', () => {
const latest = cards[0]; const latest = cards[0];
if (latest && 'speechSynthesis' in window) { if (latest && 'speechSynthesis' in window) {
const announcementKey = latest.dataset.announcementKey || ''; const announcementKey = latest.dataset.announcementKey || '';
const storedKey = window.localStorage.getItem('hospitalQueue:lastAnnouncement') || ''; const storageKey = `hospitalQueue:lastAnnouncement:${locale}`;
const storedKey = window.localStorage.getItem(storageKey) || '';
if (announcementKey && announcementKey !== storedKey) { if (announcementKey && announcementKey !== storedKey) {
const speakText = (text, lang) => { const text = locale === 'ar' ? (latest.dataset.announcementAr || '') : (latest.dataset.announcementEn || '');
if (!text) return; const utterance = new SpeechSynthesisUtterance(text);
const utterance = new SpeechSynthesisUtterance(text); utterance.lang = locale === 'ar' ? 'ar-SA' : 'en-US';
utterance.lang = lang; const voices = window.speechSynthesis.getVoices();
const voices = window.speechSynthesis.getVoices(); const preferredVoice = voices.find((voice) => voice.lang.toLowerCase().startsWith(locale === 'ar' ? 'ar' : 'en'));
const preferredVoice = voices.find((voice) => voice.lang.toLowerCase().startsWith(lang.slice(0, 2))); if (preferredVoice) utterance.voice = preferredVoice;
if (preferredVoice) utterance.voice = preferredVoice;
window.speechSynthesis.speak(utterance);
};
window.speechSynthesis.cancel(); window.speechSynthesis.cancel();
speakText(latest.dataset.announcementEn || '', 'en-US'); if (text) {
window.setTimeout(() => speakText(latest.dataset.announcementAr || '', 'ar-SA'), 1750); window.speechSynthesis.speak(utterance);
window.localStorage.setItem('hospitalQueue:lastAnnouncement', announcementKey); window.localStorage.setItem(storageKey, announcementKey);
}
} }
} }

View File

@ -6,19 +6,23 @@ qh_boot();
$activeCalls = qh_fetch_tickets(['called', 'in_progress'], null, 6); $activeCalls = qh_fetch_tickets(['called', 'in_progress'], null, 6);
$queueOverview = qh_queue_overview(); $queueOverview = qh_queue_overview();
qh_page_start('display', 'General display board', 'Public hospital queue display with bilingual callouts, text-to-speech, and an ads pane.'); qh_page_start(
'display',
qh_t('General display board', 'لوحة العرض العامة'),
qh_t('Public queue display with separated English and Arabic modes.', 'شاشة طوابير عامة مع وضعي عرض منفصلين بالعربية والإنجليزية.')
);
?> ?>
<div class="container-fluid px-3 px-lg-4 py-2" data-auto-refresh="20"> <div class="container-fluid px-3 px-lg-4 py-2" data-auto-refresh="20">
<section class="display-shell"> <section class="display-shell">
<div class="display-main panel-card p-4"> <div class="display-main panel-card p-4">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-4"> <div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-4">
<div> <div>
<div class="section-kicker">General display / الشاشة العامة</div> <div class="section-kicker"><?= qh_h(qh_t('General display', 'الشاشة العامة')) ?></div>
<h1 class="section-title-xl mt-2 mb-1">Now serving</h1> <h1 class="section-title-xl mt-2 mb-1"><?= qh_h(qh_t('Now serving', 'يتم الآن النداء')) ?></h1>
<p class="section-copy mb-0">Latest calls are read aloud in English and Arabic when supported by the browser.</p> <p class="section-copy mb-0"><?= qh_h(qh_t('This page shows a single language view. Use the language switch in the main app to open the English or Arabic display mode.', 'تعرض هذه الصفحة لغة واحدة فقط. استخدم مبدل اللغة في التطبيق لفتح وضع العرض العربي أو الإنجليزي.')) ?></p>
</div> </div>
<div class="d-flex align-items-center gap-2 flex-wrap justify-content-end"> <div class="d-flex align-items-center gap-2 flex-wrap justify-content-end">
<button type="button" class="btn btn-dark btn-sm js-fullscreen-toggle" aria-pressed="false">Full display / عرض كامل</button> <button type="button" class="btn btn-dark btn-sm js-fullscreen-toggle" aria-pressed="false" data-label-enter="<?= qh_h(qh_t('Full display', 'عرض كامل')) ?>" data-label-exit="<?= qh_h(qh_t('Exit full display', 'إنهاء العرض الكامل')) ?>"><?= qh_h(qh_t('Full display', 'عرض كامل')) ?></button>
<div class="live-clock js-live-clock"><?= qh_h(date('H:i')) ?></div> <div class="live-clock js-live-clock"><?= qh_h(date('H:i')) ?></div>
</div> </div>
</div> </div>
@ -29,7 +33,7 @@ qh_page_start('display', 'General display board', 'Public hospital queue display
<article class="announcement-card" data-announcement-key="<?= qh_h((string) $ticket['id']) ?>-<?= qh_h((string) strtotime((string) $ticket['called_at'])) ?>" data-announcement-en="<?= qh_h($speech['en']) ?>" data-announcement-ar="<?= qh_h($speech['ar']) ?>"> <article class="announcement-card" data-announcement-key="<?= qh_h((string) $ticket['id']) ?>-<?= qh_h((string) strtotime((string) $ticket['called_at'])) ?>" data-announcement-en="<?= qh_h($speech['en']) ?>" data-announcement-ar="<?= qh_h($speech['ar']) ?>">
<div> <div>
<div class="ticket-number large"><?= qh_h($ticket['ticket_number']) ?></div> <div class="ticket-number large"><?= qh_h($ticket['ticket_number']) ?></div>
<div class="display-meta"><?= qh_h($ticket['doctor_name_en'] ?? 'Doctor') ?> · Room <?= qh_h($ticket['doctor_room'] ?? '--') ?></div> <div class="display-meta"><?= qh_h(qh_name($ticket, 'doctor_name', qh_t('Doctor', 'الطبيب'))) ?> · <?= qh_h(qh_t('Room', 'غرفة')) ?> <?= qh_h($ticket['doctor_room'] ?? '--') ?></div>
</div> </div>
<div class="text-end"> <div class="text-end">
<?= qh_status_badge($ticket['status']) ?> <?= qh_status_badge($ticket['status']) ?>
@ -40,22 +44,21 @@ qh_page_start('display', 'General display board', 'Public hospital queue display
</div> </div>
<?php else: ?> <?php else: ?>
<div class="empty-state display-empty"> <div class="empty-state display-empty">
<strong>No live calls right now.</strong> <strong><?= qh_h(qh_t('No live calls right now.', 'لا توجد نداءات مباشرة حالياً.')) ?></strong>
<span>When a doctor presses “Call patient”, the ticket will appear here and play on TTS.</span> <span><?= qh_h(qh_t('When a doctor presses “Call patient”, the ticket will appear here.', 'عندما يضغط الطبيب على زر نداء المريض ستظهر التذكرة هنا.')) ?></span>
</div> </div>
<?php endif; ?> <?php endif; ?>
<div class="panel-subsection mt-4"> <div class="panel-subsection mt-4">
<h2 class="section-title mb-3">Queue by clinic / الطابور حسب العيادة</h2> <h2 class="section-title mb-3"><?= qh_h(qh_t('Queue by clinic', 'الطابور حسب العيادة')) ?></h2>
<div class="row g-3"> <div class="row g-3">
<?php foreach ($queueOverview as $row): ?> <?php foreach ($queueOverview as $row): ?>
<div class="col-md-6 col-xl-4"> <div class="col-md-6 col-xl-4">
<div class="mini-overview-card"> <div class="mini-overview-card">
<div class="fw-semibold"><?= qh_h($row['name_en']) ?></div> <div class="fw-semibold"><?= qh_h(qh_name($row)) ?></div>
<div class="small text-secondary" lang="ar" dir="rtl"><?= qh_h($row['name_ar']) ?></div>
<div class="d-flex gap-2 mt-3 flex-wrap"> <div class="d-flex gap-2 mt-3 flex-wrap">
<span class="table-pill warning">Vitals <?= qh_h((string) $row['vitals_waiting']) ?></span> <span class="table-pill warning"><?= qh_h(qh_t('Vitals', 'العلامات')) ?> <?= qh_h((string) $row['vitals_waiting']) ?></span>
<span class="table-pill info">Doctor <?= qh_h((string) $row['doctor_waiting']) ?></span> <span class="table-pill info"><?= qh_h(qh_t('Doctor', 'الطبيب')) ?> <?= qh_h((string) $row['doctor_waiting']) ?></span>
</div> </div>
</div> </div>
</div> </div>
@ -67,26 +70,26 @@ qh_page_start('display', 'General display board', 'Public hospital queue display
<aside class="display-ads panel-card p-4"> <aside class="display-ads panel-card p-4">
<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>
<div class="section-kicker">Ads & notices / الإعلانات</div> <div class="section-kicker"><?= qh_h(qh_t('Ads and notices', 'الإعلانات والتنبيهات')) ?></div>
<h2 class="section-title mb-1">Patient information</h2> <h2 class="section-title mb-1"><?= qh_h(qh_t('Patient information', 'معلومات المرضى')) ?></h2>
</div> </div>
<span class="badge rounded-pill text-bg-dark">Looping</span> <span class="badge rounded-pill text-bg-dark"><?= qh_h(qh_t('Looping', 'متكرر')) ?></span>
</div> </div>
<div class="ad-card mb-3"> <div class="ad-card mb-3">
<div class="ad-tag">Service</div> <div class="ad-tag"><?= qh_h(qh_t('Service', 'خدمة')) ?></div>
<h3>Lab packages & wellness checks</h3> <h3><?= qh_h(qh_t('Lab packages and wellness checks', 'باقات المختبر والفحوصات الوقائية')) ?></h3>
<p>Ask reception about bundled blood tests, diabetes follow-up, and annual screenings.</p> <p><?= qh_h(qh_t('Ask reception about bundled blood tests, diabetes follow-up, and annual screenings.', 'اسأل الاستقبال عن باقات تحاليل الدم، ومتابعة السكري، والفحوصات السنوية.')) ?></p>
</div> </div>
<div class="ad-card mb-3"> <div class="ad-card mb-3">
<div class="ad-tag">Reminder</div> <div class="ad-tag"><?= qh_h(qh_t('Reminder', 'تذكير')) ?></div>
<h3>Keep your ticket visible</h3> <h3><?= qh_h(qh_t('Keep your ticket visible', 'احتفظ بتذكرتك ظاهرة')) ?></h3>
<p>We announce ticket numbers on this screen and by voice. Stay near your department area.</p> <p><?= qh_h(qh_t('We announce ticket numbers on this screen and by voice. Stay near your department area.', 'نعلن أرقام التذاكر على هذه الشاشة وبالصوت. يرجى البقاء قرب منطقة القسم الخاص بك.')) ?></p>
</div> </div>
<div class="ad-card"> <div class="ad-card">
<div class="ad-tag">Wayfinding</div> <div class="ad-tag"><?= qh_h(qh_t('Wayfinding', 'الإرشاد')) ?></div>
<h3>Pharmacy & billing</h3> <h3><?= qh_h(qh_t('Pharmacy and billing', 'الصيدلية والمحاسبة')) ?></h3>
<p>Completed visits can proceed to the pharmacy and billing desk near the main exit.</p> <p><?= qh_h(qh_t('Completed visits can proceed to the pharmacy and billing desk near the main exit.', 'بعد انتهاء الزيارة يمكن التوجه إلى الصيدلية ومكتب المحاسبة قرب المخرج الرئيسي.')) ?></p>
</div> </div>
</aside> </aside>
</section> </section>

View File

@ -5,104 +5,92 @@ qh_boot();
qh_doctor_handle_request(); qh_doctor_handle_request();
$doctors = qh_fetch_doctors(); $doctors = qh_fetch_doctors();
$selectedDoctorId = isset($_GET['doctor_id']) ? (int) $_GET['doctor_id'] : ($doctors[0]['id'] ?? 0); $selectedDoctorId = isset($_GET['doctor_id']) ? (int) $_GET['doctor_id'] : (int) ($doctors[0]['id'] ?? 0);
$selectedDoctor = $selectedDoctorId ? qh_fetch_doctor($selectedDoctorId) : null; $selectedDoctor = $selectedDoctorId > 0 ? qh_fetch_doctor($selectedDoctorId) : null;
$doctorQueue = $selectedDoctorId ? qh_fetch_tickets(['ready_for_doctor', 'called', 'in_progress'], $selectedDoctorId, 20) : []; $doctorQueue = $selectedDoctorId > 0 ? qh_fetch_tickets(['ready_for_doctor', 'called', 'in_progress'], $selectedDoctorId, 20) : [];
qh_page_start('doctor', 'Doctor room control', 'Doctor queue page for calling patients, starting visits, and closing tickets.'); qh_page_start(
'doctor',
qh_t('Doctor room queue', 'طابور غرفة الطبيب'),
qh_t('Doctor workspace with separate English and Arabic page views.', 'مساحة عمل الطبيب مع صفحات عربية وإنجليزية منفصلة.')
);
?> ?>
<div class="container-xxl px-3 px-lg-4"> <div class="container-xxl px-3 px-lg-4">
<section class="page-header-panel mb-4"> <section class="page-header-panel mb-4">
<div> <div>
<span class="section-kicker">Doctor / الطبيب</span> <span class="section-kicker"><?= qh_h(qh_t('Doctor', 'الطبيب')) ?></span>
<h1 class="section-title-xl mt-2">Call the next patient and update status.</h1> <div class="locale-chip mt-3"><?= qh_h(qh_current_language_badge()) ?></div>
<p class="section-copy mb-0">The display page announces called tickets in English and Arabic using the browsers text-to-speech engine.</p> <h1 class="section-title-xl mt-2"><?= qh_h(qh_t('Call the next patient and manage room flow.', 'نادِ المريض التالي وأدر حركة الغرفة.')) ?></h1>
<p class="section-copy mb-0"><?= qh_h(qh_t('Select a doctor room, then call, start, complete, or mark no-show for the assigned patients.', 'اختر غرفة الطبيب ثم قم بنداء المرضى المخصصين أو بدء الزيارة أو إنهائها أو تسجيل عدم الحضور.')) ?></p>
</div> </div>
</section> </section>
<div class="panel-card mb-4"> <div class="row g-4">
<form method="get" class="row g-3 align-items-end"> <div class="col-xl-4">
<div class="col-lg-5"> <div class="panel-card h-100">
<label class="form-label">Doctor room / غرفة الطبيب</label> <label class="form-label"><?= qh_h(qh_t('Doctor room', 'غرفة الطبيب')) ?></label>
<select class="form-select" name="doctor_id" onchange="this.form.submit()"> <form method="get" class="vstack gap-3">
<?php foreach ($doctors as $doctor): ?> <input type="hidden" name="lang" value="<?= qh_h(qh_locale()) ?>">
<option value="<?= qh_h((string) $doctor['id']) ?>" <?= (int) $doctor['id'] === $selectedDoctorId ? 'selected' : '' ?>><?= qh_h($doctor['name_en']) ?> · <?= qh_h($doctor['clinic_name_en'] ?? '') ?> · Room <?= qh_h($doctor['room_number']) ?></option> <select class="form-select" name="doctor_id" onchange="this.form.submit()">
<?php endforeach; ?> <?php foreach ($doctors as $doctor): ?>
</select> <option value="<?= qh_h((string) $doctor['id']) ?>" <?= (int) $doctor['id'] === $selectedDoctorId ? 'selected' : '' ?>><?= qh_h(qh_name($doctor)) ?> · <?= qh_h(qh_t('Room', 'غرفة')) ?> <?= qh_h($doctor['room_number']) ?></option>
</div> <?php endforeach; ?>
<?php if ($selectedDoctor): ?> </select>
<div class="col-lg-7"> <?php if ($selectedDoctor): ?>
<div class="doctor-spotlight"> <div class="mini-overview-card">
<div> <div class="fw-semibold"><?= qh_h(qh_name($selectedDoctor)) ?></div>
<div class="fw-semibold"><?= qh_h($selectedDoctor['name_en']) ?></div> <div class="small text-secondary mt-1"><?= qh_h(qh_t('Room', 'الغرفة')) ?> <?= qh_h($selectedDoctor['room_number'] ?? '--') ?></div>
<div class="small text-secondary" lang="ar" dir="rtl"><?= qh_h($selectedDoctor['name_ar']) ?></div>
</div> </div>
<span class="room-badge">Room <?= qh_h($selectedDoctor['room_number']) ?></span> <?php endif; ?>
</div> <a class="btn btn-outline-dark" href="<?= qh_h(qh_url('display.php')) ?>" target="_blank" rel="noopener"><?= qh_h(qh_t('Preview public display', 'معاينة الشاشة العامة')) ?></a>
</div> </form>
<?php endif; ?>
</form>
</div>
<div class="panel-card">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
<div>
<h2 class="section-title mb-1">Doctor queue / طابور الطبيب</h2>
<p class="section-copy mb-0">Ready patients, live calls, and in-room consultations for the selected doctor.</p>
</div> </div>
<a class="btn btn-sm btn-outline-dark" href="display.php" target="_blank" rel="noopener">Preview public display</a>
</div> </div>
<?php if ($doctorQueue): ?> <div class="col-xl-8">
<div class="vstack gap-3"> <div class="panel-card h-100">
<?php foreach ($doctorQueue as $ticket): ?> <div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
<div class="queue-row"> <div>
<div class="queue-row-head"> <h2 class="section-title mb-1"><?= qh_h(qh_t('Doctor queue', 'طابور الطبيب')) ?></h2>
<div> <p class="section-copy mb-0"><?= qh_h(qh_t('Patients currently assigned to this room.', 'المرضى المخصصون حالياً لهذه الغرفة.')) ?></p>
<div class="ticket-code"><?= qh_h($ticket['ticket_number']) ?></div>
<div class="fw-semibold"><?= qh_h($ticket['patient_name']) ?></div>
<div class="small text-secondary">Vitals: <?= qh_h($ticket['vitals_notes'] ?: 'Not recorded / غير مسجل') ?></div>
</div>
<div class="d-flex flex-column align-items-end gap-2">
<?= qh_status_badge($ticket['status']) ?>
<a class="btn btn-sm btn-outline-dark" href="ticket.php?id=<?= qh_h((string) $ticket['id']) ?>">Ticket detail</a>
</div>
</div>
<div class="d-flex flex-wrap gap-2 mt-3">
<form method="post">
<input type="hidden" name="ticket_id" value="<?= qh_h((string) $ticket['id']) ?>">
<input type="hidden" name="doctor_id" value="<?= qh_h((string) $selectedDoctorId) ?>">
<input type="hidden" name="action" value="call_ticket">
<button class="btn btn-dark btn-sm" type="submit">Call patient</button>
</form>
<form method="post">
<input type="hidden" name="ticket_id" value="<?= qh_h((string) $ticket['id']) ?>">
<input type="hidden" name="doctor_id" value="<?= qh_h((string) $selectedDoctorId) ?>">
<input type="hidden" name="action" value="start_visit">
<button class="btn btn-outline-dark btn-sm" type="submit">Start visit</button>
</form>
<form method="post">
<input type="hidden" name="ticket_id" value="<?= qh_h((string) $ticket['id']) ?>">
<input type="hidden" name="doctor_id" value="<?= qh_h((string) $selectedDoctorId) ?>">
<input type="hidden" name="action" value="complete_ticket">
<button class="btn btn-outline-success btn-sm" type="submit">Mark done</button>
</form>
<form method="post">
<input type="hidden" name="ticket_id" value="<?= qh_h((string) $ticket['id']) ?>">
<input type="hidden" name="doctor_id" value="<?= qh_h((string) $selectedDoctorId) ?>">
<input type="hidden" name="action" value="mark_no_show">
<button class="btn btn-outline-danger btn-sm" type="submit">No-show</button>
</form>
</div>
</div> </div>
<?php endforeach; ?> <?php if ($selectedDoctor): ?>
<span class="room-badge"><?= qh_h(qh_t('Room', 'غرفة')) ?> <?= qh_h($selectedDoctor['room_number'] ?? '--') ?></span>
<?php endif; ?>
</div>
<?php if ($doctorQueue): ?>
<div class="vstack gap-3">
<?php foreach ($doctorQueue as $ticket): ?>
<article class="list-row-form">
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3">
<div>
<div class="ticket-code"><?= qh_h($ticket['ticket_number']) ?></div>
<div class="fw-semibold mt-1"><?= qh_h($ticket['patient_name']) ?></div>
<div class="small text-secondary mt-1"><?= qh_h(qh_name($ticket, 'clinic_name')) ?> · <?= qh_h(qh_t('Created', 'أُنشئت')) ?> <?= qh_format_datetime($ticket['created_at']) ?></div>
</div>
<div class="d-flex gap-2 flex-wrap align-items-center">
<?= qh_status_badge($ticket['status']) ?>
<a class="btn btn-sm btn-outline-dark" href="<?= qh_h(qh_url('ticket.php', ['id' => (int) $ticket['id']])) ?>"><?= qh_h(qh_t('Ticket detail', 'تفاصيل التذكرة')) ?></a>
</div>
</div>
<div class="d-flex flex-wrap gap-2 mt-3">
<form method="post"><input type="hidden" name="ticket_id" value="<?= qh_h((string) $ticket['id']) ?>"><input type="hidden" name="doctor_id" value="<?= qh_h((string) $selectedDoctorId) ?>"><input type="hidden" name="action" value="call_ticket"><button class="btn btn-dark btn-sm" type="submit"><?= qh_h(qh_t('Call patient', 'نداء المريض')) ?></button></form>
<form method="post"><input type="hidden" name="ticket_id" value="<?= qh_h((string) $ticket['id']) ?>"><input type="hidden" name="doctor_id" value="<?= qh_h((string) $selectedDoctorId) ?>"><input type="hidden" name="action" value="start_visit"><button class="btn btn-outline-dark btn-sm" type="submit"><?= qh_h(qh_t('Start visit', 'بدء الزيارة')) ?></button></form>
<form method="post"><input type="hidden" name="ticket_id" value="<?= qh_h((string) $ticket['id']) ?>"><input type="hidden" name="doctor_id" value="<?= qh_h((string) $selectedDoctorId) ?>"><input type="hidden" name="action" value="complete_ticket"><button class="btn btn-outline-success btn-sm" type="submit"><?= qh_h(qh_t('Mark done', 'إنهاء الزيارة')) ?></button></form>
<form method="post"><input type="hidden" name="ticket_id" value="<?= qh_h((string) $ticket['id']) ?>"><input type="hidden" name="doctor_id" value="<?= qh_h((string) $selectedDoctorId) ?>"><input type="hidden" name="action" value="mark_no_show"><button class="btn btn-outline-danger btn-sm" type="submit"><?= qh_h(qh_t('No-show', 'عدم حضور')) ?></button></form>
</div>
</article>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-state">
<strong><?= qh_h(qh_t('No patients are in this doctor queue.', 'لا يوجد مرضى في طابور هذا الطبيب.')) ?></strong>
<span><?= qh_h(qh_t('Issue a ticket at reception or complete vitals in nursing to fill this room.', 'أصدر تذكرة من الاستقبال أو أكمل العلامات الحيوية في التمريض لإضافة مرضى لهذه الغرفة.')) ?></span>
</div>
<?php endif; ?>
</div> </div>
<?php else: ?> </div>
<div class="empty-state">
<strong>No patients in this doctor queue.</strong>
<span>Issue a ticket at reception or complete vitals in nursing to fill this room.</span>
</div>
<?php endif; ?>
</div> </div>
</div> </div>
<?php qh_page_end(); ?> <?php qh_page_end(); ?>

175
index.php
View File

@ -8,48 +8,33 @@ $overview = qh_queue_overview();
$recentTickets = qh_fetch_tickets(['waiting_vitals', 'ready_for_doctor', 'called', 'in_progress'], null, 8); $recentTickets = qh_fetch_tickets(['waiting_vitals', 'ready_for_doctor', 'called', 'in_progress'], null, 8);
$calledTickets = qh_fetch_tickets(['called', 'in_progress'], null, 4); $calledTickets = qh_fetch_tickets(['called', 'in_progress'], null, 4);
qh_page_start('home', 'Hospital queue operations', 'Bilingual hospital queue dashboard with queue status, staff entry points, and public display controls.'); qh_page_start(
'home',
qh_t('Hospital queue operations', 'عمليات طابور المستشفى'),
qh_t('English operations dashboard for the hospital queue workflow.', 'لوحة عمليات عربية لمسار طابور المستشفى.')
);
?> ?>
<div class="container-xxl px-3 px-lg-4"> <div class="container-xxl px-3 px-lg-4">
<section class="hero-panel mb-4 mb-lg-5"> <section class="hero-panel mb-4 mb-lg-5">
<div class="row g-4 align-items-center"> <div class="row g-4 align-items-center">
<div class="col-lg-7"> <div class="col-lg-7">
<span class="section-kicker">Hospital queue system / نظام الطوابير</span> <span class="section-kicker"><?= qh_h(qh_t('Hospital queue system', 'نظام طوابير المستشفى')) ?></span>
<h1 class="display-title mt-3 mb-3">One bilingual workflow for reception, nursing, doctors, and the public screen.</h1> <div class="locale-chip mt-3"><?= qh_h(qh_current_language_badge()) ?></div>
<p class="lead text-secondary mb-4">Issue one ticket, route it through optional vitals, call patients to the right room, and announce the latest calls with browser text-to-speech on the general display.</p> <h1 class="display-title mt-3 mb-3"><?= qh_h(qh_t('One clear workflow for reception, nursing, doctors, and the public display.', 'مسار واضح للاستقبال والتمريض والأطباء والشاشة العامة.')) ?></h1>
<p class="lead text-secondary mb-4"><?= qh_h(qh_t('Track each patient with one ticket, route them to vitals only when needed, and keep staff and patients aligned in real time.', 'تابع كل مريض بتذكرة واحدة، ووجّهه إلى العلامات الحيوية عند الحاجة فقط، وحافظ على تنسيق العمل بين الطاقم والمرضى لحظياً.')) ?></p>
<div class="d-flex flex-wrap gap-2"> <div class="d-flex flex-wrap gap-2">
<a class="btn btn-dark" href="reception.php">Issue ticket / إصدار تذكرة</a> <a class="btn btn-dark" href="<?= qh_h(qh_url('reception.php')) ?>"><?= qh_h(qh_t('Issue ticket', 'إصدار تذكرة')) ?></a>
<a class="btn btn-outline-dark" href="display.php">Open TV display / فتح الشاشة العامة</a> <a class="btn btn-outline-dark" href="<?= qh_h(qh_url('display.php')) ?>"><?= qh_h(qh_t('Open public display', 'فتح الشاشة العامة')) ?></a>
</div> </div>
</div> </div>
<div class="col-lg-5"> <div class="col-lg-5">
<div class="hero-card stack-card h-100"> <div class="hero-card stack-card h-100">
<div class="small text-uppercase text-secondary fw-semibold mb-2">Today / اليوم</div> <div class="small text-uppercase text-secondary fw-semibold mb-2"><?= qh_h(qh_t('Today', 'اليوم')) ?></div>
<div class="row g-3"> <div class="row g-3">
<div class="col-6"> <div class="col-6"><div class="metric-card"><div class="metric-value"><?= qh_h((string) $stats['issued_today']) ?></div><div class="metric-label"><?= qh_h(qh_t('Issued tickets', 'التذاكر الصادرة')) ?></div></div></div>
<div class="metric-card"> <div class="col-6"><div class="metric-card"><div class="metric-value"><?= qh_h((string) $stats['waiting_vitals']) ?></div><div class="metric-label"><?= qh_h(qh_t('Waiting vitals', 'بانتظار العلامات')) ?></div></div></div>
<div class="metric-value"><?= qh_h((string) $stats['issued_today']) ?></div> <div class="col-6"><div class="metric-card"><div class="metric-value"><?= qh_h((string) $stats['ready_for_doctor']) ?></div><div class="metric-label"><?= qh_h(qh_t('Ready for doctor', 'جاهز للطبيب')) ?></div></div></div>
<div class="metric-label">Issued / المُصدرة</div> <div class="col-6"><div class="metric-card"><div class="metric-value"><?= qh_h((string) $stats['active_rooms']) ?></div><div class="metric-label"><?= qh_h(qh_t('Active rooms', 'الغرف النشطة')) ?></div></div></div>
</div>
</div>
<div class="col-6">
<div class="metric-card">
<div class="metric-value"><?= qh_h((string) $stats['waiting_vitals']) ?></div>
<div class="metric-label">Vitals / الحيوية</div>
</div>
</div>
<div class="col-6">
<div class="metric-card">
<div class="metric-value"><?= qh_h((string) $stats['ready_for_doctor']) ?></div>
<div class="metric-label">Ready / جاهز</div>
</div>
</div>
<div class="col-6">
<div class="metric-card">
<div class="metric-value"><?= qh_h((string) $stats['active_rooms']) ?></div>
<div class="metric-label">Active rooms / الغرف النشطة</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -61,29 +46,26 @@ qh_page_start('home', 'Hospital queue operations', 'Bilingual hospital queue das
<div class="panel-card h-100"> <div class="panel-card h-100">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3"> <div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
<div> <div>
<h2 class="section-title mb-1">Live queue overview / نظرة مباشرة على الطابور</h2> <h2 class="section-title mb-1"><?= qh_h(qh_t('Live queue overview', 'نظرة مباشرة على الطابور')) ?></h2>
<p class="section-copy mb-0">Clinic-level demand for vitals, doctor readiness, and active calls.</p> <p class="section-copy mb-0"><?= qh_h(qh_t('See clinic demand, waiting load, and active calls at a glance.', 'اطلع بسرعة على ضغط العيادات والانتظار والنداءات النشطة.')) ?></p>
</div> </div>
<a class="btn btn-sm btn-outline-dark" href="admin.php">Manage clinics & doctors</a> <a class="btn btn-sm btn-outline-dark" href="<?= qh_h(qh_url('admin.php')) ?>"><?= qh_h(qh_t('Manage clinics and doctors', 'إدارة العيادات والأطباء')) ?></a>
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table align-middle mb-0"> <table class="table align-middle mb-0">
<thead> <thead>
<tr> <tr>
<th>Clinic / العيادة</th> <th><?= qh_h(qh_t('Clinic', 'العيادة')) ?></th>
<th>Vitals wait</th> <th><?= qh_h(qh_t('Vitals wait', 'انتظار العلامات')) ?></th>
<th>Doctor wait</th> <th><?= qh_h(qh_t('Doctor wait', 'انتظار الطبيب')) ?></th>
<th>Active calls</th> <th><?= qh_h(qh_t('Active calls', 'النداءات النشطة')) ?></th>
<th>Total today</th> <th><?= qh_h(qh_t('Today total', 'إجمالي اليوم')) ?></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($overview as $row): ?> <?php foreach ($overview as $row): ?>
<tr> <tr>
<td> <td class="fw-semibold"><?= qh_h(qh_name($row)) ?></td>
<div class="fw-semibold"><?= qh_h($row['name_en']) ?></div>
<div class="small text-secondary" lang="ar" dir="rtl"><?= qh_h($row['name_ar']) ?></div>
</td>
<td><span class="table-pill warning"><?= qh_h((string) $row['vitals_waiting']) ?></span></td> <td><span class="table-pill warning"><?= qh_h((string) $row['vitals_waiting']) ?></span></td>
<td><span class="table-pill info"><?= qh_h((string) $row['doctor_waiting']) ?></span></td> <td><span class="table-pill info"><?= qh_h((string) $row['doctor_waiting']) ?></span></td>
<td><span class="table-pill dark"><?= qh_h((string) $row['active_calls']) ?></span></td> <td><span class="table-pill dark"><?= qh_h((string) $row['active_calls']) ?></span></td>
@ -97,19 +79,15 @@ qh_page_start('home', 'Hospital queue operations', 'Bilingual hospital queue das
</div> </div>
<div class="col-xl-4"> <div class="col-xl-4">
<div class="panel-card h-100"> <div class="panel-card h-100">
<div class="d-flex justify-content-between align-items-center mb-3"> <h2 class="section-title mb-1"><?= qh_h(qh_t('Current calls', 'النداءات الحالية')) ?></h2>
<div> <p class="section-copy mb-3"><?= qh_h(qh_t('Patients already called into active doctor rooms.', 'المرضى الذين تم استدعاؤهم بالفعل إلى غرف الأطباء.')) ?></p>
<h2 class="section-title mb-1">Current calls / النداءات الحالية</h2>
<p class="section-copy mb-0">Patients already called to a doctor room.</p>
</div>
</div>
<?php if ($calledTickets): ?> <?php if ($calledTickets): ?>
<div class="vstack gap-3"> <div class="vstack gap-3">
<?php foreach ($calledTickets as $ticket): ?> <?php foreach ($calledTickets as $ticket): ?>
<div class="call-strip"> <div class="call-strip">
<div> <div>
<div class="ticket-code"><?= qh_h($ticket['ticket_number']) ?></div> <div class="ticket-code"><?= qh_h($ticket['ticket_number']) ?></div>
<div class="small text-secondary"><?= qh_h($ticket['doctor_name_en'] ?? 'Unassigned') ?> · Room <?= qh_h($ticket['doctor_room'] ?? '--') ?></div> <div class="small text-secondary"><?= qh_h(qh_name($ticket, 'doctor_name', qh_t('Unassigned', 'غير محدد'))) ?> · <?= qh_h(qh_t('Room', 'غرفة')) ?> <?= qh_h($ticket['doctor_room'] ?? '--') ?></div>
</div> </div>
<?= qh_status_badge($ticket['status']) ?> <?= qh_status_badge($ticket['status']) ?>
</div> </div>
@ -117,8 +95,8 @@ qh_page_start('home', 'Hospital queue operations', 'Bilingual hospital queue das
</div> </div>
<?php else: ?> <?php else: ?>
<div class="empty-state compact"> <div class="empty-state compact">
<strong>No active calls yet.</strong> <strong><?= qh_h(qh_t('No active calls yet.', 'لا توجد نداءات نشطة حالياً.')) ?></strong>
<span>Use the doctor page to call the next patient.</span> <span><?= qh_h(qh_t('Use the doctor page to call the next patient.', 'استخدم صفحة الطبيب لنداء المريض التالي.')) ?></span>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
@ -126,81 +104,64 @@ qh_page_start('home', 'Hospital queue operations', 'Bilingual hospital queue das
</section> </section>
<section class="row g-4 mb-4 mb-lg-5"> <section class="row g-4 mb-4 mb-lg-5">
<div class="col-md-6 col-xl-3"> <?php
<a class="workflow-card h-100" href="admin.php"> $workflow = [
<span class="workflow-step">01</span> ['step' => '01', 'title' => qh_t('Reception issues one ticket', 'الاستقبال يصدر تذكرة واحدة'), 'copy' => qh_t('Choose the clinic and doctor once at the front desk.', 'يتم اختيار العيادة والطبيب مرة واحدة من مكتب الاستقبال.')],
<h3>Admin config</h3> ['step' => '02', 'title' => qh_t('Optional vitals step', 'خطوة العلامات الحيوية عند الحاجة'), 'copy' => qh_t('Only clinics that require vitals route through nursing first.', 'فقط العيادات التي تتطلب العلامات الحيوية تمر أولاً على التمريض.')],
<p>Manage clinics, vitals requirement, doctors, and room assignments.</p> ['step' => '03', 'title' => qh_t('Doctor calls the patient', 'الطبيب ينادي المريض'), 'copy' => qh_t('The doctor room page pushes the next call to the display.', 'صفحة غرفة الطبيب ترسل النداء التالي إلى الشاشة.')],
</a> ['step' => '04', 'title' => qh_t('Display announces the call', 'الشاشة تعلن النداء'), 'copy' => qh_t('Patients see the latest ticket and room assignment immediately.', 'يرى المرضى آخر تذكرة ورقم الغرفة مباشرة.')],
</div> ];
<div class="col-md-6 col-xl-3"> ?>
<a class="workflow-card h-100" href="reception.php"> <?php foreach ($workflow as $card): ?>
<span class="workflow-step">02</span> <div class="col-md-6 col-xl-3">
<h3>Reception issue</h3> <article class="panel-card workflow-card h-100">
<p>Create a single bilingual ticket and route it to vitals or directly to the doctor.</p> <span class="workflow-step"><?= qh_h($card['step']) ?></span>
</a> <h3><?= qh_h($card['title']) ?></h3>
</div> <p><?= qh_h($card['copy']) ?></p>
<div class="col-md-6 col-xl-3"> </article>
<a class="workflow-card h-100" href="nursing.php"> </div>
<span class="workflow-step">03</span> <?php endforeach; ?>
<h3>Nursing handoff</h3>
<p>Capture vitals, note key measurements, and release the patient to the doctor queue.</p>
</a>
</div>
<div class="col-md-6 col-xl-3">
<a class="workflow-card h-100" href="doctor.php">
<span class="workflow-step">04</span>
<h3>Doctor call</h3>
<p>Call, start, and complete visits while the TV display announces the latest ticket.</p>
</a>
</div>
</section> </section>
<section class="panel-card"> <section class="panel-card">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3"> <div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
<div> <div>
<h2 class="section-title mb-1">Recent patient flow / آخر حركة للمرضى</h2> <h2 class="section-title mb-1"><?= qh_h(qh_t('Recent patient flow', 'آخر حركة للمرضى')) ?></h2>
<p class="section-copy mb-0">Each ticket is linked to a detail page showing its current stage.</p> <p class="section-copy mb-0"><?= qh_h(qh_t('The newest in-progress tickets across the workflow.', 'أحدث التذاكر النشطة عبر مسار العمل.')) ?></p>
</div> </div>
<a class="btn btn-sm btn-outline-dark" href="reception.php">Create new ticket</a> <a class="btn btn-sm btn-outline-dark" href="<?= qh_h(qh_url('reception.php')) ?>"><?= qh_h(qh_t('Create new ticket', 'إنشاء تذكرة جديدة')) ?></a>
</div> </div>
<?php if ($recentTickets): ?> <?php if ($recentTickets): ?>
<div class="table-responsive"> <div class="table-responsive">
<table class="table align-middle mb-0"> <table class="table align-middle mb-0">
<thead> <thead>
<tr> <tr>
<th>Ticket</th> <th><?= qh_h(qh_t('Ticket', 'التذكرة')) ?></th>
<th>Patient</th> <th><?= qh_h(qh_t('Patient', 'المريض')) ?></th>
<th>Clinic</th> <th><?= qh_h(qh_t('Clinic', 'العيادة')) ?></th>
<th>Doctor / Room</th> <th><?= qh_h(qh_t('Doctor', 'الطبيب')) ?></th>
<th>Status</th> <th><?= qh_h(qh_t('Status', 'الحالة')) ?></th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($recentTickets as $ticket): ?> <?php foreach ($recentTickets as $ticket): ?>
<tr> <tr>
<td class="fw-semibold"><?= qh_h($ticket['ticket_number']) ?></td> <td class="fw-semibold"><?= qh_h($ticket['ticket_number']) ?></td>
<td> <td><?= qh_h($ticket['patient_name']) ?></td>
<div><?= qh_h($ticket['patient_name']) ?></div> <td><?= qh_h(qh_name($ticket, 'clinic_name')) ?></td>
<div class="small text-secondary"><?= strtoupper(qh_h($ticket['language_pref'])) ?></div> <td><?= qh_h(qh_name($ticket, 'doctor_name', qh_t('Unassigned', 'غير محدد'))) ?></td>
</td> <td><?= qh_status_badge($ticket['status']) ?></td>
<td> <td class="text-end"><a class="btn btn-sm btn-outline-dark" href="<?= qh_h(qh_url('ticket.php', ['id' => (int) $ticket['id']])) ?>"><?= qh_h(qh_t('View detail', 'عرض التفاصيل')) ?></a></td>
<div><?= qh_h($ticket['clinic_name_en'] ?? '—') ?></div> </tr>
<div class="small text-secondary" lang="ar" dir="rtl"><?= qh_h($ticket['clinic_name_ar'] ?? '') ?></div> <?php endforeach; ?>
</td>
<td><?= qh_h($ticket['doctor_name_en'] ?? '—') ?> <span class="text-secondary">· <?= qh_h($ticket['doctor_room'] ?? '--') ?></span></td>
<td><?= qh_status_badge($ticket['status']) ?></td>
<td class="text-end"><a class="btn btn-sm btn-outline-dark" href="ticket.php?id=<?= qh_h((string) $ticket['id']) ?>">View detail</a></td>
</tr>
<?php endforeach; ?>
</tbody> </tbody>
</table> </table>
</div> </div>
<?php else: ?> <?php else: ?>
<div class="empty-state"> <div class="empty-state compact">
<strong>No active tickets yet.</strong> <strong><?= qh_h(qh_t('No recent tickets yet.', 'لا توجد تذاكر حديثة حتى الآن.')) ?></strong>
<span>Start from the reception desk to issue the first patient ticket.</span> <span><?= qh_h(qh_t('Start from reception to populate the workflow.', 'ابدأ من صفحة الاستقبال لبدء تعبئة مسار العمل.')) ?></span>
</div> </div>
<?php endif; ?> <?php endif; ?>
</section> </section>

View File

@ -5,58 +5,64 @@ qh_boot();
qh_nursing_handle_request(); qh_nursing_handle_request();
$waitingTickets = qh_fetch_tickets(['waiting_vitals'], null, 20); $waitingTickets = qh_fetch_tickets(['waiting_vitals'], null, 20);
qh_page_start('nursing', 'Nursing vitals queue', 'Nursing queue for clinics that require vitals before the doctor visit.');
qh_page_start(
'nursing',
qh_t('Nursing vitals queue', 'طابور التمريض للعلامات الحيوية'),
qh_t('Nursing workspace with separate English and Arabic views.', 'مساحة عمل التمريض مع فصل بين الواجهة العربية والإنجليزية.')
);
?> ?>
<div class="container-xxl px-3 px-lg-4"> <div class="container-xxl px-3 px-lg-4">
<section class="page-header-panel mb-4"> <section class="page-header-panel mb-4">
<div> <div>
<span class="section-kicker">Nursing / التمريض</span> <span class="section-kicker"><?= qh_h(qh_t('Nursing', 'التمريض')) ?></span>
<h1 class="section-title-xl mt-2">Capture vitals and release to doctor.</h1> <div class="locale-chip mt-3"><?= qh_h(qh_current_language_badge()) ?></div>
<p class="section-copy mb-0">Only tickets from clinics marked “requires vitals” arrive here. Once notes are saved, the same ticket continues to the doctor queue.</p> <h1 class="section-title-xl mt-2"><?= qh_h(qh_t('Capture vitals and release patients to doctors.', 'سجّل العلامات الحيوية ثم حوّل المرضى إلى الأطباء.')) ?></h1>
<p class="section-copy mb-0"><?= qh_h(qh_t('Only clinics configured for vitals-first appear here.', 'تظهر هنا فقط العيادات التي تم إعدادها لتبدأ بالعلامات الحيوية.')) ?></p>
</div> </div>
</section> </section>
<div class="panel-card"> <div class="panel-card">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3"> <div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
<div> <div>
<h2 class="section-title mb-1">Waiting for vitals / بانتظار العلامات الحيوية</h2> <h2 class="section-title mb-1"><?= qh_h(qh_t('Waiting for vitals', 'بانتظار العلامات الحيوية')) ?></h2>
<p class="section-copy mb-0">Add a short clinical note to transfer the patient to the assigned doctor.</p> <p class="section-copy mb-0"><?= qh_h(qh_t('Add a short note, then send the patient to the doctor queue.', 'أضف ملاحظة قصيرة ثم أرسل المريض إلى طابور الطبيب.')) ?></p>
</div> </div>
<span class="badge text-bg-warning px-3 py-2"><?= qh_h((string) count($waitingTickets)) ?> patients</span> <span class="badge rounded-pill text-bg-dark"><?= qh_h(count($waitingTickets) . ' ' . qh_t('patients', 'مرضى')) ?></span>
</div> </div>
<?php if ($waitingTickets): ?> <?php if ($waitingTickets): ?>
<div class="vstack gap-3"> <div class="vstack gap-3">
<?php foreach ($waitingTickets as $ticket): ?> <?php foreach ($waitingTickets as $ticket): ?>
<div class="queue-row"> <article class="list-row-form">
<div class="queue-row-head"> <div class="d-flex justify-content-between align-items-start flex-wrap gap-3">
<div> <div>
<div class="ticket-code"><?= qh_h($ticket['ticket_number']) ?></div> <div class="ticket-code"><?= qh_h($ticket['ticket_number']) ?></div>
<div class="fw-semibold"><?= qh_h($ticket['patient_name']) ?></div> <div class="fw-semibold mt-1"><?= qh_h($ticket['patient_name']) ?></div>
<div class="small text-secondary"><?= qh_h($ticket['clinic_name_en'] ?? '') ?> → <?= qh_h($ticket['doctor_name_en'] ?? '') ?> · Room <?= qh_h($ticket['doctor_room'] ?? '--') ?></div> <div class="small text-secondary mt-1"><?= qh_h(qh_name($ticket, 'clinic_name')) ?> · <?= qh_h(qh_name($ticket, 'doctor_name')) ?> · <?= qh_h(qh_t('Room', 'غرفة')) ?> <?= qh_h($ticket['doctor_room'] ?? '--') ?></div>
</div> </div>
<div class="d-flex flex-column align-items-end gap-2"> <div class="d-flex gap-2 flex-wrap align-items-center">
<?= qh_status_badge($ticket['status']) ?> <?= qh_status_badge($ticket['status']) ?>
<a class="btn btn-sm btn-outline-dark" href="ticket.php?id=<?= qh_h((string) $ticket['id']) ?>">Ticket detail</a> <a class="btn btn-sm btn-outline-dark" href="<?= qh_h(qh_url('ticket.php', ['id' => (int) $ticket['id']])) ?>"><?= qh_h(qh_t('Ticket detail', 'تفاصيل التذكرة')) ?></a>
</div> </div>
</div> </div>
<form method="post" class="row g-3 align-items-end mt-2"> <form method="post" class="row g-3 align-items-end mt-1">
<input type="hidden" name="ticket_id" value="<?= qh_h((string) $ticket['id']) ?>"> <input type="hidden" name="ticket_id" value="<?= qh_h((string) $ticket['id']) ?>">
<div class="col-lg-9"> <div class="col-lg-9">
<label class="form-label">Vitals note / ملاحظة العلامات الحيوية</label> <label class="form-label"><?= qh_h(qh_t('Vitals note', 'ملاحظة العلامات الحيوية')) ?></label>
<input class="form-control" type="text" name="vitals_notes" placeholder="BP 120/80 · Temp 36.8 · Weight 68kg" required> <textarea class="form-control" name="vitals_notes" rows="2" placeholder="<?= qh_h(qh_t('Blood pressure, pulse, temperature...', 'الضغط والنبض والحرارة...')) ?>" required></textarea>
</div> </div>
<div class="col-lg-3 d-grid"> <div class="col-lg-3 d-grid">
<button class="btn btn-dark" type="submit">Send to doctor</button> <button class="btn btn-dark" type="submit"><?= qh_h(qh_t('Send to doctor', 'إرسال إلى الطبيب')) ?></button>
</div> </div>
</form> </form>
</div> </article>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<?php else: ?> <?php else: ?>
<div class="empty-state"> <div class="empty-state">
<strong>No patients waiting for vitals.</strong> <strong><?= qh_h(qh_t('No patients are waiting for vitals.', 'لا يوجد مرضى بانتظار العلامات الحيوية.')) ?></strong>
<span>Tickets from vitals-required clinics will appear here automatically after issue.</span> <span><?= qh_h(qh_t('New vitals-first tickets from reception will appear here.', 'ستظهر هنا التذاكر الجديدة التي تتطلب العلامات الحيوية أولاً.')) ?></span>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>

View File

@ -16,6 +16,7 @@ function qh_boot(): void
qh_ensure_schema(); qh_ensure_schema();
qh_seed_demo_data(); qh_seed_demo_data();
qh_seed_hospital_profile();
$booted = true; $booted = true;
} }
@ -51,6 +52,31 @@ CREATE TABLE IF NOT EXISTS hospital_queue_records (
SQL; SQL;
db()->exec($sql); db()->exec($sql);
$profileSql = <<<SQL
CREATE TABLE IF NOT EXISTS hospital_profile_settings (
id TINYINT UNSIGNED NOT NULL PRIMARY KEY,
name_en VARCHAR(160) DEFAULT NULL,
name_ar VARCHAR(160) DEFAULT NULL,
short_name VARCHAR(40) DEFAULT NULL,
tagline_en VARCHAR(255) DEFAULT NULL,
tagline_ar VARCHAR(255) DEFAULT NULL,
phone VARCHAR(60) DEFAULT NULL,
email VARCHAR(160) DEFAULT NULL,
website VARCHAR(255) DEFAULT NULL,
address_en VARCHAR(255) DEFAULT NULL,
address_ar VARCHAR(255) DEFAULT NULL,
working_hours_en VARCHAR(255) DEFAULT NULL,
working_hours_ar VARCHAR(255) DEFAULT NULL,
logo_url VARCHAR(255) DEFAULT NULL,
favicon_url VARCHAR(255) DEFAULT NULL,
primary_color VARCHAR(7) DEFAULT NULL,
secondary_color VARCHAR(7) DEFAULT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
SQL;
db()->exec($profileSql);
} }
function qh_seed_demo_data(): void function qh_seed_demo_data(): void
@ -128,6 +154,40 @@ function qh_seed_demo_data(): void
} }
} }
function qh_seed_hospital_profile(): void
{
$exists = (int) db()->query('SELECT COUNT(*) FROM hospital_profile_settings')->fetchColumn();
if ($exists > 0) {
return;
}
$stmt = db()->prepare(
'INSERT INTO hospital_profile_settings
(id, name_en, name_ar, short_name, tagline_en, tagline_ar, phone, email, website, address_en, address_ar, working_hours_en, working_hours_ar, logo_url, favicon_url, primary_color, secondary_color)
VALUES
(1, :name_en, :name_ar, :short_name, :tagline_en, :tagline_ar, :phone, :email, :website, :address_en, :address_ar, :working_hours_en, :working_hours_ar, :logo_url, :favicon_url, :primary_color, :secondary_color)'
);
$stmt->execute([
'name_en' => qh_project_name('Hospital Queue Center'),
'name_ar' => 'مركز إدارة الطوابير',
'short_name' => 'HQC',
'tagline_en' => 'Organized patient flow, queue control, and staff coordination.',
'tagline_ar' => 'تنظيم تدفق المرضى وإدارة الطوابير وتنسيق عمل الطاقم.',
'phone' => '',
'email' => '',
'website' => '',
'address_en' => '',
'address_ar' => '',
'working_hours_en' => 'SunThu · 8:00 AM 8:00 PM',
'working_hours_ar' => 'الأحد الخميس · 8:00 ص 8:00 م',
'logo_url' => '',
'favicon_url' => '',
'primary_color' => '#0f8b8d',
'secondary_color' => '#16697a',
]);
}
function qh_h(?string $value): string function qh_h(?string $value): string
{ {
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
@ -151,29 +211,313 @@ function qh_asset_version(string $relativePath): int
return is_file($fullPath) ? (int) filemtime($fullPath) : time(); return is_file($fullPath) ? (int) filemtime($fullPath) : time();
} }
function qh_locale(): string
{
static $locale = null;
if ($locale !== null) {
return $locale;
}
$requested = strtolower(trim((string) ($_GET['lang'] ?? '')));
if (in_array($requested, ['en', 'ar'], true)) {
$_SESSION['qh_lang'] = $requested;
}
$sessionLocale = strtolower(trim((string) ($_SESSION['qh_lang'] ?? 'en')));
$locale = in_array($sessionLocale, ['en', 'ar'], true) ? $sessionLocale : 'en';
return $locale;
}
function qh_is_ar(): bool
{
return qh_locale() === 'ar';
}
function qh_t(string $en, string $ar): string
{
return qh_is_ar() ? $ar : $en;
}
function qh_locale_label(?string $locale = null): string
{
$locale = $locale ?: qh_locale();
return $locale === 'ar' ? 'العربية' : 'English';
}
function qh_other_locale(): string
{
return qh_is_ar() ? 'en' : 'ar';
}
function qh_url(string $path, array $params = []): string
{
$target = basename($path);
$params['lang'] = $params['lang'] ?? qh_locale();
$query = http_build_query($params);
return $query !== '' ? $target . '?' . $query : $target;
}
function qh_switch_lang_url(string $locale): string
{
$params = $_GET;
$params['lang'] = in_array($locale, ['en', 'ar'], true) ? $locale : qh_other_locale();
$target = basename((string) ($_SERVER['PHP_SELF'] ?? 'index.php'));
$query = http_build_query($params);
return $query !== '' ? $target . '?' . $query : $target;
}
function qh_name(array $row, string $base = 'name', string $fallback = '—'): string
{
$primaryKey = $base . '_' . (qh_is_ar() ? 'ar' : 'en');
$secondaryKey = $base . '_' . (qh_is_ar() ? 'en' : 'ar');
$primary = trim((string) ($row[$primaryKey] ?? ''));
if ($primary !== '') {
return $primary;
}
$secondary = trim((string) ($row[$secondaryKey] ?? ''));
return $secondary !== '' ? $secondary : $fallback;
}
function qh_fetch_hospital_profile(): array
{
static $profile = null;
if ($profile !== null) {
return $profile;
}
$row = db()->query('SELECT * FROM hospital_profile_settings WHERE id = 1 LIMIT 1')->fetch();
$profile = is_array($row) ? $row : [];
return $profile;
}
function qh_hospital_name(): string
{
$profile = qh_fetch_hospital_profile();
$name = qh_name($profile, 'name', '');
if ($name !== '') {
return $name;
}
$shortName = trim((string) ($profile['short_name'] ?? ''));
return $shortName !== '' ? $shortName : qh_project_name();
}
function qh_hospital_tagline(): string
{
$profile = qh_fetch_hospital_profile();
return qh_name($profile, 'tagline', '');
}
function qh_hospital_logo_url(): string
{
$profile = qh_fetch_hospital_profile();
return trim((string) ($profile['logo_url'] ?? ''));
}
function qh_hospital_favicon_url(): string
{
$profile = qh_fetch_hospital_profile();
$favicon = trim((string) ($profile['favicon_url'] ?? ''));
if ($favicon !== '') {
return $favicon;
}
return qh_hospital_logo_url();
}
function qh_hospital_contact_value(string $field): string
{
$profile = qh_fetch_hospital_profile();
return trim((string) ($profile[$field] ?? ''));
}
function qh_hospital_brand_initials(): string
{
$profile = qh_fetch_hospital_profile();
$source = trim((string) ($profile['short_name'] ?? ''));
if ($source === '') {
$source = trim((string) ($profile['name_en'] ?? qh_project_name('Hospital Queue')));
}
$parts = preg_split('/[^\p{L}\p{N}]+/u', $source, -1, PREG_SPLIT_NO_EMPTY);
$initials = [];
foreach ($parts as $part) {
if (preg_match('/^[\p{L}\p{N}]/u', $part, $matches)) {
$initials[] = strtoupper($matches[0]);
}
if (count($initials) >= 2) {
break;
}
}
return $initials !== [] ? implode('', array_slice($initials, 0, 2)) : 'HQ';
}
function qh_sanitize_hex_color(?string $value, string $fallback): string
{
$candidate = strtoupper(trim((string) $value));
return preg_match('/^#[0-9A-F]{6}$/', $candidate) ? $candidate : $fallback;
}
function qh_hospital_primary_color(): string
{
$profile = qh_fetch_hospital_profile();
return qh_sanitize_hex_color($profile['primary_color'] ?? null, '#0F8B8D');
}
function qh_hospital_secondary_color(): string
{
$profile = qh_fetch_hospital_profile();
return qh_sanitize_hex_color($profile['secondary_color'] ?? null, '#16697A');
}
function qh_current_language_badge(): string
{
return qh_t('English workspace', 'واجهة عربية');
}
function qh_admin_sections(): array
{
return [
'admin.php' => [
'label' => qh_t('Overview', 'نظرة عامة'),
'description' => qh_t('Admin home and setup summary.', 'الصفحة الرئيسية وملخص الإعدادات.'),
'icon' => 'overview',
],
'admin_hospital.php' => [
'label' => qh_t('Hospital profile', 'ملف المستشفى'),
'description' => qh_t('Manage logo, favicon, contact details, and brand colors.', 'إدارة الشعار والأيقونة وبيانات التواصل وألوان الهوية.'),
'icon' => 'hospital',
],
'admin_clinics.php' => [
'label' => qh_t('Clinics', 'العيادات'),
'description' => qh_t('Manage clinic codes, routing, and order.', 'إدارة رموز العيادات والمسار والترتيب.'),
'icon' => 'clinic',
],
'admin_doctors.php' => [
'label' => qh_t('Doctors', 'الأطباء'),
'description' => qh_t('Manage doctors, rooms, and assignments.', 'إدارة الأطباء والغرف والتعيينات.'),
'icon' => 'doctor',
],
];
}
function qh_admin_allowed_pages(): array
{
return array_keys(qh_admin_sections());
}
function qh_admin_return_to(?string $candidate = null): string
{
$page = basename((string) ($candidate ?? 'admin.php'));
return in_array($page, qh_admin_allowed_pages(), true) ? $page : 'admin.php';
}
function qh_admin_sidebar_icon(string $icon): string
{
return match ($icon) {
'hospital' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 20V7l7-3 7 3v13"/><path d="M9 11h1"/><path d="M14 11h1"/><path d="M11 8h2"/><path d="M11 20v-4h2v4"/><path d="M3 20h18"/></svg>',
'clinic' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20h16"/><path d="M6 20V8l6-4 6 4v12"/><path d="M9 12h1"/><path d="M14 12h1"/><path d="M11 20v-4h2v4"/></svg>',
'doctor' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/><circle cx="12" cy="12" r="8.5"/></svg>',
default => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="4" width="6.5" height="6.5" rx="1.5"/><rect x="13.5" y="4" width="6.5" height="6.5" rx="1.5"/><rect x="4" y="13.5" width="6.5" height="6.5" rx="1.5"/><rect x="13.5" y="13.5" width="6.5" height="6.5" rx="1.5"/></svg>',
};
}
function qh_admin_stats(): array
{
$clinics = qh_fetch_clinics();
$doctors = qh_fetch_doctors();
$vitalsClinics = count(array_filter($clinics, static fn(array $clinic): bool => (int) ($clinic['requires_vitals'] ?? 0) === 1));
return [
'clinics' => count($clinics),
'doctors' => count($doctors),
'vitals_clinics' => $vitalsClinics,
'direct_clinics' => max(count($clinics) - $vitalsClinics, 0),
];
}
function qh_render_admin_sidebar(string $activePage, array $stats = []): void
{
$sections = qh_admin_sections();
$activePage = qh_admin_return_to($activePage);
if ($stats === []) {
$stats = qh_admin_stats();
}
echo '<div class="panel-card admin-sidebar">';
echo ' <div class="admin-sidebar-top">';
echo ' <span class="section-kicker">' . qh_h(qh_t('Admin panel', 'لوحة الإدارة')) . '</span>';
echo ' <h1 class="section-title mt-3 mb-1">' . qh_h(qh_hospital_name()) . '</h1>';
echo ' <p class="section-copy mb-0">' . qh_h(qh_hospital_tagline() !== '' ? qh_hospital_tagline() : qh_t('Move between dedicated pages instead of one long mixed admin screen.', 'تنقل بين صفحات مستقلة بدلاً من شاشة إدارة طويلة ومختلطة.')) . '</p>';
echo ' </div>';
echo ' <nav class="admin-sidebar-nav" aria-label="Admin section navigation">';
foreach ($sections as $file => $section) {
$activeClass = $file === $activePage ? ' active' : '';
$icon = qh_admin_sidebar_icon((string) ($section['icon'] ?? 'overview'));
echo ' <a class="admin-sidebar-link' . $activeClass . '" href="' . qh_h(qh_url($file)) . '" title="' . qh_h($section['description']) . '">';
echo ' <span class="admin-sidebar-link-icon">' . $icon . '</span>';
echo ' <span class="admin-sidebar-link-text">' . qh_h($section['label']) . '</span>';
echo ' </a>';
}
echo ' </nav>';
echo ' <div class="admin-sidebar-meta">';
echo ' <div class="admin-mini-stat"><span class="admin-mini-stat-value">' . qh_h((string) ($stats['clinics'] ?? 0)) . '</span><span class="admin-mini-stat-label">' . qh_h(qh_t('Clinics', 'العيادات')) . '</span></div>';
echo ' <div class="admin-mini-stat"><span class="admin-mini-stat-value">' . qh_h((string) ($stats['doctors'] ?? 0)) . '</span><span class="admin-mini-stat-label">' . qh_h(qh_t('Doctors', 'الأطباء')) . '</span></div>';
echo ' <div class="admin-mini-stat"><span class="admin-mini-stat-value">' . qh_h((string) ($stats['vitals_clinics'] ?? 0)) . '</span><span class="admin-mini-stat-label">' . qh_h(qh_t('Vitals-first clinics', 'العيادات التي تبدأ بالعلامات')) . '</span></div>';
echo ' </div>';
echo '</div>';
}
function qh_ticket_next_stop(array $ticket): string
{
return (int) ($ticket['clinic_requires_vitals'] ?? 0) === 1
? qh_t('Nursing vitals', 'العلامات الحيوية في التمريض')
: qh_t('Doctor waiting area', 'منطقة انتظار الطبيب');
}
function qh_ticket_last_note(array $ticket): string
{
return match ((string) ($ticket['status'] ?? '')) {
'waiting_vitals' => qh_t('Proceed to nursing vitals first.', 'يرجى التوجه أولاً إلى العلامات الحيوية في التمريض.'),
'ready_for_doctor' => qh_t('Wait for the doctor call on the public display.', 'انتظر نداء الطبيب على الشاشة العامة.'),
'called' => qh_t('The patient has been called to the doctor room.', 'تم نداء المريض إلى غرفة الطبيب.'),
'in_progress' => qh_t('The consultation is currently in progress.', 'الاستشارة جارية حالياً.'),
'done' => qh_t('The visit has been completed.', 'تم إكمال الزيارة.'),
'no_show' => qh_t('The patient was marked as no-show.', 'تم تسجيل المريض كحالة عدم حضور.'),
default => trim((string) ($ticket['display_note'] ?? '')) !== '' ? (string) $ticket['display_note'] : '—',
};
}
function qh_label(string $en, string $ar, string $wrapper = 'span'): string function qh_label(string $en, string $ar, string $wrapper = 'span'): string
{ {
$tag = preg_replace('/[^a-z0-9]/i', '', $wrapper) ?: 'span'; $tag = preg_replace('/[^a-z0-9]/i', '', $wrapper) ?: 'span';
return sprintf( return sprintf('<%1$s>%2$s</%1$s>', $tag, qh_h(qh_t($en, $ar)));
'<%1$s class="bi-label"><span class="label-en">%2$s</span><span class="label-sep"> / </span><span class="label-ar" lang="ar" dir="rtl">%3$s</span></%1$s>',
$tag,
qh_h($en),
qh_h($ar)
);
} }
function qh_page_start(string $activePage, string $pageTitle, string $metaDescription = ''): void function qh_page_start(string $activePage, string $pageTitle, string $metaDescription = ''): void
{ {
$locale = qh_locale();
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? qh_project_description(); $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? qh_project_description();
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
$fullTitle = $pageTitle . ' · ' . qh_project_name(); $brandName = qh_hospital_name();
$faviconUrl = qh_hospital_favicon_url();
$fullTitle = $pageTitle . ' · ' . $brandName;
$description = $metaDescription !== '' ? $metaDescription : qh_project_description(); $description = $metaDescription !== '' ? $metaDescription : qh_project_description();
$bodyClass = 'page-' . preg_replace('/[^a-z0-9\-]/i', '-', $activePage); $bodyClass = 'page-' . preg_replace('/[^a-z0-9\-]/i', '-', $activePage) . ' lang-' . $locale;
$assetVersionCss = qh_asset_version('assets/css/custom.css'); $assetVersionCss = qh_asset_version('assets/css/custom.css');
$assetVersionJs = qh_asset_version('assets/js/main.js'); $primaryColor = qh_hospital_primary_color();
$secondaryColor = qh_hospital_secondary_color();
echo '<!doctype html>'; echo '<!doctype html>';
echo '<html lang="en">'; echo '<html lang="' . qh_h($locale) . '" dir="' . (qh_is_ar() ? 'rtl' : 'ltr') . '">';
echo '<head>'; echo '<head>';
echo ' <meta charset="utf-8">'; echo ' <meta charset="utf-8">';
echo ' <meta name="viewport" content="width=device-width, initial-scale=1">'; echo ' <meta name="viewport" content="width=device-width, initial-scale=1">';
@ -187,11 +531,16 @@ function qh_page_start(string $activePage, string $pageTitle, string $metaDescri
echo ' <meta property="og:image" content="' . qh_h((string) $projectImageUrl) . '">'; echo ' <meta property="og:image" content="' . qh_h((string) $projectImageUrl) . '">';
echo ' <meta property="twitter:image" content="' . qh_h((string) $projectImageUrl) . '">'; echo ' <meta property="twitter:image" content="' . qh_h((string) $projectImageUrl) . '">';
} }
echo ' <meta name="theme-color" content="#f3f4f6">'; if ($faviconUrl !== '') {
echo ' <link rel="icon" href="' . qh_h($faviconUrl) . '">';
echo ' <link rel="apple-touch-icon" href="' . qh_h($faviconUrl) . '">';
}
echo ' <meta name="theme-color" content="' . qh_h($primaryColor) . '">';
echo ' <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">'; echo ' <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">';
echo ' <link rel="stylesheet" href="assets/css/custom.css?v=' . $assetVersionCss . '">'; echo ' <link rel="stylesheet" href="assets/css/custom.css?v=' . $assetVersionCss . '">';
echo ' <style>:root{--accent:' . qh_h($primaryColor) . ';--accent-strong:' . qh_h($secondaryColor) . ';}</style>';
echo '</head>'; echo '</head>';
echo '<body class="' . qh_h($bodyClass) . '" data-page="' . qh_h($activePage) . '">'; echo '<body class="' . qh_h($bodyClass) . '" data-page="' . qh_h($activePage) . '" data-locale="' . qh_h($locale) . '">';
if ($activePage !== 'display') { if ($activePage !== 'display') {
qh_render_nav($activePage); qh_render_nav($activePage);
} }
@ -199,6 +548,8 @@ function qh_page_start(string $activePage, string $pageTitle, string $metaDescri
qh_render_flash(); qh_render_flash();
} }
function qh_page_end(): void function qh_page_end(): void
{ {
$assetVersionJs = qh_asset_version('assets/js/main.js'); $assetVersionJs = qh_asset_version('assets/js/main.js');
@ -211,37 +562,57 @@ function qh_page_end(): void
function qh_render_nav(string $activePage): void function qh_render_nav(string $activePage): void
{ {
$links = [ $links = [
'home' => ['href' => 'index.php', 'label' => qh_label('Operations', 'العمليات')], 'home' => ['href' => qh_url('index.php'), 'label' => qh_t('Operations', 'العمليات')],
'admin' => ['href' => 'admin.php', 'label' => qh_label('Admin', 'الإدارة')], 'admin' => ['href' => qh_url('admin.php'), 'label' => qh_t('Admin', 'الإدارة')],
'reception' => ['href' => 'reception.php', 'label' => qh_label('Reception', 'الاستقبال')], 'reception' => ['href' => qh_url('reception.php'), 'label' => qh_t('Reception', 'الاستقبال')],
'nursing' => ['href' => 'nursing.php', 'label' => qh_label('Nursing', 'التمريض')], 'nursing' => ['href' => qh_url('nursing.php'), 'label' => qh_t('Nursing', 'التمريض')],
'doctor' => ['href' => 'doctor.php', 'label' => qh_label('Doctor', 'الطبيب')], 'doctor' => ['href' => qh_url('doctor.php'), 'label' => qh_t('Doctor', 'الطبيب')],
'display' => ['href' => 'display.php', 'label' => qh_label('Display', 'الشاشة العامة')], 'display' => ['href' => qh_url('display.php'), 'label' => qh_t('Display', 'الشاشة')],
]; ];
echo '<header class="border-bottom bg-white sticky-top shadow-sm">'; $logoUrl = qh_hospital_logo_url();
$tagline = qh_hospital_tagline();
echo '<header class="app-header border-bottom sticky-top shadow-sm">';
echo ' <nav class="navbar navbar-expand-lg">'; echo ' <nav class="navbar navbar-expand-lg">';
echo ' <div class="container-fluid container-xxl px-3 px-lg-4">'; echo ' <div class="container-fluid container-xxl px-3 px-lg-4">';
echo ' <a class="navbar-brand d-flex flex-column gap-0" href="index.php">'; echo ' <a class="navbar-brand d-flex align-items-center" href="' . qh_h(qh_url('index.php')) . '">';
echo ' <span class="brand-mark">HQ</span>'; if ($logoUrl !== '') {
echo ' <span class="brand-text">' . qh_h(qh_project_name()) . '</span>'; echo ' <span class="brand-mark brand-mark-image"><img src="' . qh_h($logoUrl) . '" alt="' . qh_h(qh_hospital_name()) . '"></span>';
} else {
echo ' <span class="brand-mark">' . qh_h(qh_hospital_brand_initials()) . '</span>';
}
echo ' <span class="brand-copy">';
echo ' <span class="brand-text">' . qh_h(qh_hospital_name()) . '</span>';
if ($tagline !== '') {
echo ' <span class="brand-subtext">' . qh_h($tagline) . '</span>';
}
echo ' </span>';
echo ' </a>'; echo ' </a>';
echo ' <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#appNav" aria-controls="appNav" aria-expanded="false" aria-label="Toggle navigation">'; echo ' <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#appNav" aria-controls="appNav" aria-expanded="false" aria-label="' . qh_h(qh_t('Toggle navigation', 'تبديل التنقل')) . '">';
echo ' <span class="navbar-toggler-icon"></span>'; echo ' <span class="navbar-toggler-icon"></span>';
echo ' </button>'; echo ' </button>';
echo ' <div class="collapse navbar-collapse" id="appNav">'; echo ' <div class="collapse navbar-collapse" id="appNav">';
echo ' <ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2 mt-3 mt-lg-0">'; echo ' <ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2 mt-3 mt-lg-0">';
foreach ($links as $key => $link) { foreach ($links as $key => $link) {
$activeClass = $key === $activePage ? ' active' : ''; $activeClass = $key === $activePage ? ' active' : '';
echo ' <li class="nav-item"><a class="nav-link' . $activeClass . '" href="' . qh_h($link['href']) . '">' . $link['label'] . '</a></li>'; echo ' <li class="nav-item"><a class="nav-link' . $activeClass . '" href="' . qh_h($link['href']) . '">' . qh_h($link['label']) . '</a></li>';
} }
echo ' </ul>'; echo ' </ul>';
echo ' <div class="lang-switcher ms-lg-3 mt-3 mt-lg-0">';
foreach (['en', 'ar'] as $lang) {
$activeClass = qh_locale() === $lang ? ' active' : '';
echo ' <a class="lang-switch-link' . $activeClass . '" href="' . qh_h(qh_switch_lang_url($lang)) . '">' . qh_h(qh_locale_label($lang)) . '</a>';
}
echo ' </div>';
echo ' </div>'; echo ' </div>';
echo ' </div>'; echo ' </div>';
echo ' </nav>'; echo ' </nav>';
echo '</header>'; echo '</header>';
} }
function qh_set_flash(string $type, string $message): void function qh_set_flash(string $type, string $message): void
{ {
$_SESSION['flash'] = ['type' => $type, 'message' => $message]; $_SESSION['flash'] = ['type' => $type, 'message' => $message];
@ -268,18 +639,27 @@ function qh_render_flash(): void
echo ' <div class="toast app-toast js-app-toast text-bg-' . qh_h($toastType) . ' border-0" role="status" aria-live="polite" aria-atomic="true">'; echo ' <div class="toast app-toast js-app-toast text-bg-' . qh_h($toastType) . ' border-0" role="status" aria-live="polite" aria-atomic="true">';
echo ' <div class="d-flex">'; echo ' <div class="d-flex">';
echo ' <div class="toast-body">' . qh_h((string) $flash['message']) . '</div>'; echo ' <div class="toast-body">' . qh_h((string) $flash['message']) . '</div>';
echo ' <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>'; echo ' <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="' . qh_h(qh_t('Close', 'إغلاق')) . '"></button>';
echo ' </div>'; echo ' </div>';
echo ' </div>'; echo ' </div>';
echo '</div>'; echo '</div>';
} }
function qh_redirect(string $location): void function qh_redirect(string $location): void
{ {
if (strpos($location, 'lang=') === false) {
$separator = str_contains($location, '?') ? '&' : '?';
$location .= $separator . 'lang=' . qh_locale();
}
header('Location: ' . $location); header('Location: ' . $location);
exit; exit;
} }
function qh_fetch_clinics(): array function qh_fetch_clinics(): array
{ {
$stmt = db()->query("SELECT * FROM hospital_queue_records WHERE item_type = 'clinic' ORDER BY sort_order ASC, name_en ASC"); $stmt = db()->query("SELECT * FROM hospital_queue_records WHERE item_type = 'clinic' ORDER BY sort_order ASC, name_en ASC");
@ -398,12 +778,12 @@ function qh_create_ticket(string $patientName, int $clinicId, int $doctorId, str
$pdo = db(); $pdo = db();
$clinic = qh_fetch_clinic($clinicId); $clinic = qh_fetch_clinic($clinicId);
if (!$clinic) { if (!$clinic) {
throw new RuntimeException('Clinic not found.'); throw new RuntimeException(qh_t('Clinic not found.', 'لم يتم العثور على العيادة.'));
} }
$doctor = qh_fetch_doctor($doctorId); $doctor = qh_fetch_doctor($doctorId);
if (!$doctor || (int) $doctor['clinic_id'] !== $clinicId) { if (!$doctor || (int) $doctor['clinic_id'] !== $clinicId) {
throw new RuntimeException('Doctor does not belong to the selected clinic.'); throw new RuntimeException(qh_t('The doctor does not belong to the selected clinic.', 'الطبيب لا يتبع العيادة المحددة.'));
} }
$ticketNumber = qh_generate_ticket_number($clinic['code']); $ticketNumber = qh_generate_ticket_number($clinic['code']);
@ -477,9 +857,11 @@ function qh_status_meta(string $status): array
function qh_status_badge(string $status): string function qh_status_badge(string $status): string
{ {
$meta = qh_status_meta($status); $meta = qh_status_meta($status);
return '<span class="badge text-bg-' . qh_h($meta['class']) . ' status-badge">' . qh_label($meta['en'], $meta['ar']) . '</span>'; return '<span class="badge text-bg-' . qh_h($meta['class']) . ' status-badge">' . qh_h(qh_t($meta['en'], $meta['ar'])) . '</span>';
} }
function qh_call_message(array $ticket): array function qh_call_message(array $ticket): array
{ {
$ticketNumber = $ticket['ticket_number'] ?? '---'; $ticketNumber = $ticket['ticket_number'] ?? '---';
@ -505,18 +887,25 @@ function qh_format_datetime(?string $value): string
function qh_require_post(): void function qh_require_post(): void
{ {
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
qh_set_flash('danger', 'Invalid request method.'); qh_set_flash('danger', qh_t('Invalid request method.', 'طريقة الطلب غير صالحة.'));
qh_redirect('index.php'); qh_redirect('index.php');
} }
} }
function qh_admin_handle_request(): void function qh_admin_handle_request(): void
{ {
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
return; return;
} }
$action = $_POST['action'] ?? ''; $action = trim((string) ($_POST['action'] ?? ''));
if ($action === '') {
return;
}
$returnTo = qh_admin_return_to($_POST['return_to'] ?? 'admin.php');
$pdo = db(); $pdo = db();
try { try {
@ -526,7 +915,7 @@ function qh_admin_handle_request(): void
$nameAr = trim((string) ($_POST['name_ar'] ?? '')); $nameAr = trim((string) ($_POST['name_ar'] ?? ''));
$requiresVitals = isset($_POST['requires_vitals']) ? 1 : 0; $requiresVitals = isset($_POST['requires_vitals']) ? 1 : 0;
if ($code === '' || $nameEn === '' || $nameAr === '') { if ($code === '' || $nameEn === '' || $nameAr === '') {
throw new InvalidArgumentException('Please complete the clinic code and bilingual names.'); throw new InvalidArgumentException(qh_t('Please complete the clinic code and both clinic names.', 'يرجى إدخال رمز العيادة والاسمين الإنجليزي والعربي.'));
} }
$stmt = $pdo->prepare( $stmt = $pdo->prepare(
"INSERT INTO hospital_queue_records (item_type, code, name_en, name_ar, requires_vitals, sort_order, status) "INSERT INTO hospital_queue_records (item_type, code, name_en, name_ar, requires_vitals, sort_order, status)
@ -537,29 +926,59 @@ function qh_admin_handle_request(): void
'name_en' => $nameEn, 'name_en' => $nameEn,
'name_ar' => $nameAr, 'name_ar' => $nameAr,
'requires_vitals' => $requiresVitals, 'requires_vitals' => $requiresVitals,
'sort_order' => (int) ($_POST['sort_order'] ?? 50), 'sort_order' => max((int) ($_POST['sort_order'] ?? 50), 1),
]); ]);
qh_set_flash('success', 'Clinic saved successfully.'); qh_set_flash('success', qh_t('Clinic saved successfully.', 'تم حفظ العيادة بنجاح.'));
} elseif ($action === 'update_clinic') { } elseif ($action === 'update_clinic') {
$clinicId = (int) ($_POST['clinic_id'] ?? 0); $clinicId = (int) ($_POST['clinic_id'] ?? 0);
$code = strtoupper(trim((string) ($_POST['code'] ?? '')));
$nameEn = trim((string) ($_POST['name_en'] ?? ''));
$nameAr = trim((string) ($_POST['name_ar'] ?? ''));
if ($clinicId <= 0 || $code === '' || $nameEn === '' || $nameAr === '') {
throw new InvalidArgumentException(qh_t('Please complete the clinic details before updating.', 'يرجى إكمال بيانات العيادة قبل التحديث.'));
}
$stmt = $pdo->prepare( $stmt = $pdo->prepare(
"UPDATE hospital_queue_records "UPDATE hospital_queue_records
SET requires_vitals = :requires_vitals, sort_order = :sort_order SET code = :code,
name_en = :name_en,
name_ar = :name_ar,
requires_vitals = :requires_vitals,
sort_order = :sort_order
WHERE item_type = 'clinic' AND id = :clinic_id" WHERE item_type = 'clinic' AND id = :clinic_id"
); );
$stmt->execute([ $stmt->execute([
'code' => substr($code, 0, 10),
'name_en' => $nameEn,
'name_ar' => $nameAr,
'requires_vitals' => isset($_POST['requires_vitals']) ? 1 : 0, 'requires_vitals' => isset($_POST['requires_vitals']) ? 1 : 0,
'sort_order' => (int) ($_POST['sort_order'] ?? 50), 'sort_order' => max((int) ($_POST['sort_order'] ?? 50), 1),
'clinic_id' => $clinicId, 'clinic_id' => $clinicId,
]); ]);
qh_set_flash('success', 'Clinic settings updated.'); qh_set_flash('success', qh_t('Clinic updated successfully.', 'تم تحديث العيادة بنجاح.'));
} elseif ($action === 'delete_clinic') {
$clinicId = (int) ($_POST['clinic_id'] ?? 0);
if ($clinicId <= 0) {
throw new InvalidArgumentException(qh_t('Invalid clinic selected.', 'تم اختيار عيادة غير صالحة.'));
}
$doctorCountStmt = $pdo->prepare("SELECT COUNT(*) FROM hospital_queue_records WHERE item_type = 'doctor' AND clinic_id = :clinic_id");
$doctorCountStmt->execute(['clinic_id' => $clinicId]);
$doctorCount = (int) $doctorCountStmt->fetchColumn();
$ticketCountStmt = $pdo->prepare("SELECT COUNT(*) FROM hospital_queue_records WHERE item_type = 'ticket' AND clinic_id = :clinic_id");
$ticketCountStmt->execute(['clinic_id' => $clinicId]);
$ticketCount = (int) $ticketCountStmt->fetchColumn();
if ($doctorCount > 0 || $ticketCount > 0) {
throw new InvalidArgumentException(qh_t('This clinic cannot be deleted because it is linked to doctors or patient tickets.', 'لا يمكن حذف هذه العيادة لأنها مرتبطة بأطباء أو تذاكر مرضى.'));
}
$stmt = $pdo->prepare("DELETE FROM hospital_queue_records WHERE item_type = 'clinic' AND id = :clinic_id");
$stmt->execute(['clinic_id' => $clinicId]);
qh_set_flash('success', qh_t('Clinic deleted successfully.', 'تم حذف العيادة بنجاح.'));
} elseif ($action === 'add_doctor') { } elseif ($action === 'add_doctor') {
$nameEn = trim((string) ($_POST['name_en'] ?? '')); $nameEn = trim((string) ($_POST['name_en'] ?? ''));
$nameAr = trim((string) ($_POST['name_ar'] ?? '')); $nameAr = trim((string) ($_POST['name_ar'] ?? ''));
$clinicId = (int) ($_POST['clinic_id'] ?? 0); $clinicId = (int) ($_POST['clinic_id'] ?? 0);
$roomNumber = trim((string) ($_POST['room_number'] ?? '')); $roomNumber = trim((string) ($_POST['room_number'] ?? ''));
if ($nameEn === '' || $nameAr === '' || $clinicId <= 0 || $roomNumber === '') { if ($nameEn === '' || $nameAr === '' || $clinicId <= 0 || $roomNumber === '') {
throw new InvalidArgumentException('Please complete the doctor form before saving.'); throw new InvalidArgumentException(qh_t('Please complete the doctor form before saving.', 'يرجى إكمال بيانات الطبيب قبل الحفظ.'));
} }
$stmt = $pdo->prepare( $stmt = $pdo->prepare(
"INSERT INTO hospital_queue_records (item_type, name_en, name_ar, clinic_id, room_number, sort_order, status) "INSERT INTO hospital_queue_records (item_type, name_en, name_ar, clinic_id, room_number, sort_order, status)
@ -570,31 +989,129 @@ function qh_admin_handle_request(): void
'name_ar' => $nameAr, 'name_ar' => $nameAr,
'clinic_id' => $clinicId, 'clinic_id' => $clinicId,
'room_number' => $roomNumber, 'room_number' => $roomNumber,
'sort_order' => (int) ($_POST['sort_order'] ?? 50), 'sort_order' => max((int) ($_POST['sort_order'] ?? 50), 1),
]); ]);
qh_set_flash('success', 'Doctor profile saved.'); qh_set_flash('success', qh_t('Doctor profile saved.', 'تم حفظ ملف الطبيب.'));
} elseif ($action === 'update_doctor') { } elseif ($action === 'update_doctor') {
$doctorId = (int) ($_POST['doctor_id'] ?? 0); $doctorId = (int) ($_POST['doctor_id'] ?? 0);
$nameEn = trim((string) ($_POST['name_en'] ?? ''));
$nameAr = trim((string) ($_POST['name_ar'] ?? ''));
$clinicId = (int) ($_POST['clinic_id'] ?? 0); $clinicId = (int) ($_POST['clinic_id'] ?? 0);
$roomNumber = trim((string) ($_POST['room_number'] ?? '')); $roomNumber = trim((string) ($_POST['room_number'] ?? ''));
if ($doctorId <= 0 || $nameEn === '' || $nameAr === '' || $clinicId <= 0 || $roomNumber === '') {
throw new InvalidArgumentException(qh_t('Please complete the doctor details before updating.', 'يرجى إكمال بيانات الطبيب قبل التحديث.'));
}
$stmt = $pdo->prepare( $stmt = $pdo->prepare(
"UPDATE hospital_queue_records "UPDATE hospital_queue_records
SET clinic_id = :clinic_id, room_number = :room_number, sort_order = :sort_order SET name_en = :name_en,
name_ar = :name_ar,
clinic_id = :clinic_id,
room_number = :room_number,
sort_order = :sort_order
WHERE item_type = 'doctor' AND id = :doctor_id" WHERE item_type = 'doctor' AND id = :doctor_id"
); );
$stmt->execute([ $stmt->execute([
'name_en' => $nameEn,
'name_ar' => $nameAr,
'clinic_id' => $clinicId, 'clinic_id' => $clinicId,
'room_number' => $roomNumber, 'room_number' => $roomNumber,
'sort_order' => (int) ($_POST['sort_order'] ?? 50), 'sort_order' => max((int) ($_POST['sort_order'] ?? 50), 1),
'doctor_id' => $doctorId, 'doctor_id' => $doctorId,
]); ]);
qh_set_flash('success', 'Doctor assignment updated.'); qh_set_flash('success', qh_t('Doctor assignment updated.', 'تم تحديث تعيين الطبيب.'));
} elseif ($action === 'delete_doctor') {
$doctorId = (int) ($_POST['doctor_id'] ?? 0);
if ($doctorId <= 0) {
throw new InvalidArgumentException(qh_t('Invalid doctor selected.', 'تم اختيار طبيب غير صالح.'));
}
$ticketCountStmt = $pdo->prepare("SELECT COUNT(*) FROM hospital_queue_records WHERE item_type = 'ticket' AND doctor_id = :doctor_id");
$ticketCountStmt->execute(['doctor_id' => $doctorId]);
$ticketCount = (int) $ticketCountStmt->fetchColumn();
if ($ticketCount > 0) {
throw new InvalidArgumentException(qh_t('This doctor cannot be deleted because patient tickets are linked to the profile.', 'لا يمكن حذف هذا الطبيب لأن هناك تذاكر مرضى مرتبطة بالملف.'));
}
$stmt = $pdo->prepare("DELETE FROM hospital_queue_records WHERE item_type = 'doctor' AND id = :doctor_id");
$stmt->execute(['doctor_id' => $doctorId]);
qh_set_flash('success', qh_t('Doctor deleted successfully.', 'تم حذف الطبيب بنجاح.'));
} elseif ($action === 'save_hospital_profile') {
$nameEn = trim((string) ($_POST['name_en'] ?? ''));
$nameAr = trim((string) ($_POST['name_ar'] ?? ''));
$shortName = trim((string) ($_POST['short_name'] ?? ''));
$taglineEn = trim((string) ($_POST['tagline_en'] ?? ''));
$taglineAr = trim((string) ($_POST['tagline_ar'] ?? ''));
$phone = trim((string) ($_POST['phone'] ?? ''));
$email = trim((string) ($_POST['email'] ?? ''));
$website = trim((string) ($_POST['website'] ?? ''));
$addressEn = trim((string) ($_POST['address_en'] ?? ''));
$addressAr = trim((string) ($_POST['address_ar'] ?? ''));
$workingHoursEn = trim((string) ($_POST['working_hours_en'] ?? ''));
$workingHoursAr = trim((string) ($_POST['working_hours_ar'] ?? ''));
$logoUrl = trim((string) ($_POST['logo_url'] ?? ''));
$faviconUrl = trim((string) ($_POST['favicon_url'] ?? ''));
$primaryColor = qh_sanitize_hex_color($_POST['primary_color'] ?? '', '#0F8B8D');
$secondaryColor = qh_sanitize_hex_color($_POST['secondary_color'] ?? '', '#16697A');
if ($nameEn === '' || $nameAr === '') {
throw new InvalidArgumentException(qh_t('Please enter both the English and Arabic hospital names.', 'يرجى إدخال اسم المستشفى بالإنجليزية والعربية.'));
}
if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
throw new InvalidArgumentException(qh_t('Please enter a valid hospital email address.', 'يرجى إدخال بريد إلكتروني صالح للمستشفى.'));
}
if ($website !== '' && filter_var($website, FILTER_VALIDATE_URL) === false) {
throw new InvalidArgumentException(qh_t('Please enter a valid website URL.', 'يرجى إدخال رابط موقع صالح.'));
}
if ($logoUrl !== '' && filter_var($logoUrl, FILTER_VALIDATE_URL) === false) {
throw new InvalidArgumentException(qh_t('Please enter a valid logo URL.', 'يرجى إدخال رابط صالح للشعار.'));
}
if ($faviconUrl !== '' && filter_var($faviconUrl, FILTER_VALIDATE_URL) === false) {
throw new InvalidArgumentException(qh_t('Please enter a valid favicon URL.', 'يرجى إدخال رابط صالح للأيقونة.'));
}
$stmt = $pdo->prepare(
"UPDATE hospital_profile_settings
SET name_en = :name_en,
name_ar = :name_ar,
short_name = :short_name,
tagline_en = :tagline_en,
tagline_ar = :tagline_ar,
phone = :phone,
email = :email,
website = :website,
address_en = :address_en,
address_ar = :address_ar,
working_hours_en = :working_hours_en,
working_hours_ar = :working_hours_ar,
logo_url = :logo_url,
favicon_url = :favicon_url,
primary_color = :primary_color,
secondary_color = :secondary_color
WHERE id = 1"
);
$stmt->execute([
'name_en' => $nameEn,
'name_ar' => $nameAr,
'short_name' => substr($shortName, 0, 40),
'tagline_en' => $taglineEn,
'tagline_ar' => $taglineAr,
'phone' => $phone,
'email' => $email,
'website' => $website,
'address_en' => $addressEn,
'address_ar' => $addressAr,
'working_hours_en' => $workingHoursEn,
'working_hours_ar' => $workingHoursAr,
'logo_url' => $logoUrl,
'favicon_url' => $faviconUrl,
'primary_color' => $primaryColor,
'secondary_color' => $secondaryColor,
]);
qh_set_flash('success', qh_t('Hospital profile updated successfully.', 'تم تحديث ملف المستشفى بنجاح.'));
} }
} catch (Throwable $exception) { } catch (Throwable $exception) {
qh_set_flash('danger', $exception->getMessage()); qh_set_flash('danger', $exception->getMessage());
} }
qh_redirect('admin.php'); qh_redirect($returnTo);
} }
function qh_reception_handle_request(): void function qh_reception_handle_request(): void
@ -610,11 +1127,11 @@ function qh_reception_handle_request(): void
$languagePref = trim((string) ($_POST['language_pref'] ?? 'en')); $languagePref = trim((string) ($_POST['language_pref'] ?? 'en'));
if ($patientName === '' || $clinicId <= 0 || $doctorId <= 0) { if ($patientName === '' || $clinicId <= 0 || $doctorId <= 0) {
throw new InvalidArgumentException('Please complete patient name, clinic, and doctor.'); throw new InvalidArgumentException(qh_t('Please complete the patient name, clinic, and doctor.', 'يرجى إدخال اسم المريض والعيادة والطبيب.'));
} }
$ticketId = qh_create_ticket($patientName, $clinicId, $doctorId, $languagePref); $ticketId = qh_create_ticket($patientName, $clinicId, $doctorId, $languagePref);
qh_set_flash('success', 'Ticket issued successfully.'); qh_set_flash('success', qh_t('Ticket issued successfully.', 'تم إصدار التذكرة بنجاح.'));
qh_redirect('reception.php?ticket_id=' . $ticketId); qh_redirect('reception.php?ticket_id=' . $ticketId);
} catch (Throwable $exception) { } catch (Throwable $exception) {
qh_set_flash('danger', $exception->getMessage()); qh_set_flash('danger', $exception->getMessage());
@ -632,7 +1149,7 @@ function qh_nursing_handle_request(): void
$ticketId = (int) ($_POST['ticket_id'] ?? 0); $ticketId = (int) ($_POST['ticket_id'] ?? 0);
$vitalsNotes = trim((string) ($_POST['vitals_notes'] ?? '')); $vitalsNotes = trim((string) ($_POST['vitals_notes'] ?? ''));
if ($ticketId <= 0 || $vitalsNotes === '') { if ($ticketId <= 0 || $vitalsNotes === '') {
throw new InvalidArgumentException('Please add a short vitals note before sending the patient forward.'); throw new InvalidArgumentException(qh_t('Please add a short vitals note before sending the patient forward.', 'يرجى إضافة ملاحظة قصيرة للعلامات الحيوية قبل إرسال المريض.'));
} }
$stmt = db()->prepare( $stmt = db()->prepare(
@ -646,7 +1163,7 @@ function qh_nursing_handle_request(): void
'vitals_notes' => $vitalsNotes, 'vitals_notes' => $vitalsNotes,
'ticket_id' => $ticketId, 'ticket_id' => $ticketId,
]); ]);
qh_set_flash('success', 'Vitals captured and patient moved to doctor queue.'); qh_set_flash('success', qh_t('Vitals captured and patient moved to the doctor queue.', 'تم حفظ العلامات الحيوية ونقل المريض إلى طابور الطبيب.'));
} catch (Throwable $exception) { } catch (Throwable $exception) {
qh_set_flash('danger', $exception->getMessage()); qh_set_flash('danger', $exception->getMessage());
} }
@ -666,7 +1183,7 @@ function qh_doctor_handle_request(): void
$action = trim((string) ($_POST['action'] ?? '')); $action = trim((string) ($_POST['action'] ?? ''));
$ticket = qh_fetch_ticket($ticketId); $ticket = qh_fetch_ticket($ticketId);
if (!$ticket || $doctorId <= 0 || (int) $ticket['doctor_id'] !== $doctorId) { if (!$ticket || $doctorId <= 0 || (int) $ticket['doctor_id'] !== $doctorId) {
throw new InvalidArgumentException('That ticket is not available for the selected doctor.'); throw new InvalidArgumentException(qh_t('That ticket is not available for the selected doctor.', 'هذه التذكرة غير متاحة للطبيب المحدد.'));
} }
if ($action === 'call_ticket') { if ($action === 'call_ticket') {
@ -680,7 +1197,7 @@ function qh_doctor_handle_request(): void
'display_note' => $message['en'], 'display_note' => $message['en'],
'ticket_id' => $ticketId, 'ticket_id' => $ticketId,
]); ]);
qh_set_flash('success', 'Patient call pushed to the public display.'); qh_set_flash('success', qh_t('Patient call was sent to the public display.', 'تم إرسال نداء المريض إلى الشاشة العامة.'));
} elseif ($action === 'start_visit') { } elseif ($action === 'start_visit') {
$stmt = db()->prepare( $stmt = db()->prepare(
"UPDATE hospital_queue_records "UPDATE hospital_queue_records
@ -688,7 +1205,7 @@ function qh_doctor_handle_request(): void
WHERE item_type = 'ticket' AND id = :ticket_id" WHERE item_type = 'ticket' AND id = :ticket_id"
); );
$stmt->execute(['ticket_id' => $ticketId]); $stmt->execute(['ticket_id' => $ticketId]);
qh_set_flash('success', 'Consultation started.'); qh_set_flash('success', qh_t('Consultation started.', 'بدأت الاستشارة.'));
} elseif ($action === 'complete_ticket') { } elseif ($action === 'complete_ticket') {
$stmt = db()->prepare( $stmt = db()->prepare(
"UPDATE hospital_queue_records "UPDATE hospital_queue_records
@ -696,7 +1213,7 @@ function qh_doctor_handle_request(): void
WHERE item_type = 'ticket' AND id = :ticket_id" WHERE item_type = 'ticket' AND id = :ticket_id"
); );
$stmt->execute(['ticket_id' => $ticketId]); $stmt->execute(['ticket_id' => $ticketId]);
qh_set_flash('success', 'Visit marked as completed.'); qh_set_flash('success', qh_t('Visit marked as completed.', 'تم إنهاء الزيارة.'));
} elseif ($action === 'mark_no_show') { } elseif ($action === 'mark_no_show') {
$stmt = db()->prepare( $stmt = db()->prepare(
"UPDATE hospital_queue_records "UPDATE hospital_queue_records
@ -704,7 +1221,7 @@ function qh_doctor_handle_request(): void
WHERE item_type = 'ticket' AND id = :ticket_id" WHERE item_type = 'ticket' AND id = :ticket_id"
); );
$stmt->execute(['ticket_id' => $ticketId]); $stmt->execute(['ticket_id' => $ticketId]);
qh_set_flash('warning', 'Patient marked as no-show.'); qh_set_flash('warning', qh_t('Patient marked as no-show.', 'تم تسجيل المريض كحالة عدم حضور.'));
} }
qh_redirect('doctor.php?doctor_id=' . $doctorId); qh_redirect('doctor.php?doctor_id=' . $doctorId);

View File

@ -9,54 +9,57 @@ $doctors = qh_fetch_doctors();
$currentTicket = isset($_GET['ticket_id']) ? qh_fetch_ticket((int) $_GET['ticket_id']) : null; $currentTicket = isset($_GET['ticket_id']) ? qh_fetch_ticket((int) $_GET['ticket_id']) : null;
$todayTickets = qh_fetch_tickets(['waiting_vitals', 'ready_for_doctor', 'called', 'in_progress', 'done', 'no_show'], null, 12); $todayTickets = qh_fetch_tickets(['waiting_vitals', 'ready_for_doctor', 'called', 'in_progress', 'done', 'no_show'], null, 12);
qh_page_start('reception', 'Reception ticket issuance', 'Reception desk ticket issue flow with one ticket that follows the patient through vitals and doctor visit.'); qh_page_start(
'reception',
qh_t('Reception ticket issuance', 'إصدار التذاكر في الاستقبال'),
qh_t('Reception page with separated language views.', 'صفحة الاستقبال مع فصل واضح بين اللغتين.')
);
?> ?>
<div class="container-xxl px-3 px-lg-4"> <div class="container-xxl px-3 px-lg-4">
<section class="page-header-panel mb-4"> <section class="page-header-panel mb-4">
<div> <div>
<span class="section-kicker">Reception / الاستقبال</span> <span class="section-kicker"><?= qh_h(qh_t('Reception', 'الاستقبال')) ?></span>
<h1 class="section-title-xl mt-2">Issue one ticket for the full visit.</h1> <div class="locale-chip mt-3"><?= qh_h(qh_current_language_badge()) ?></div>
<p class="section-copy mb-0">Reception decides the clinic and doctor once. The system automatically routes the patient to vitals first only when the selected clinic requires it.</p> <h1 class="section-title-xl mt-2"><?= qh_h(qh_t('Issue one ticket for the full visit.', 'أصدر تذكرة واحدة لكامل الزيارة.')) ?></h1>
<p class="section-copy mb-0"><?= qh_h(qh_t('Reception selects the clinic and doctor once. The system routes to nursing only when the clinic requires vitals.', 'يقوم الاستقبال باختيار العيادة والطبيب مرة واحدة. ويوجه النظام إلى التمريض فقط عندما تتطلب العيادة العلامات الحيوية.')) ?></p>
</div> </div>
</section> </section>
<div class="row g-4"> <div class="row g-4">
<div class="col-xl-5"> <div class="col-xl-5">
<div class="panel-card h-100"> <div class="panel-card h-100">
<h2 class="section-title mb-3">New patient ticket / تذكرة مريض جديدة</h2> <h2 class="section-title mb-3"><?= qh_h(qh_t('New patient ticket', 'تذكرة مريض جديدة')) ?></h2>
<form method="post" class="vstack gap-3" novalidate> <form method="post" class="vstack gap-3" novalidate>
<div> <div>
<label class="form-label">Patient name / اسم المريض</label> <label class="form-label"><?= qh_h(qh_t('Patient name', 'اسم المريض')) ?></label>
<input class="form-control" type="text" name="patient_name" placeholder="Maha Ali" required> <input class="form-control" type="text" name="patient_name" placeholder="Maha Ali" required>
</div> </div>
<div> <div>
<label class="form-label">Clinic / العيادة</label> <label class="form-label"><?= qh_h(qh_t('Clinic', 'العيادة')) ?></label>
<select class="form-select js-clinic-select" name="clinic_id" required> <select class="form-select js-clinic-select" name="clinic_id" required>
<option value="">Choose clinic</option> <option value=""><?= qh_h(qh_t('Choose clinic', 'اختر العيادة')) ?></option>
<?php foreach ($clinics as $clinic): ?> <?php foreach ($clinics as $clinic): ?>
<option value="<?= qh_h((string) $clinic['id']) ?>"><?= qh_h($clinic['name_en']) ?> / <?= qh_h($clinic['name_ar']) ?><?= (int) $clinic['requires_vitals'] === 1 ? ' · Vitals first' : '' ?></option> <option value="<?= qh_h((string) $clinic['id']) ?>"><?= qh_h(qh_name($clinic)) ?><?php if ((int) $clinic['requires_vitals'] === 1): ?> · <?= qh_h(qh_t('Vitals first', 'العلامات أولاً')) ?><?php endif; ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
<div> <div>
<label class="form-label">Doctor / الطبيب</label> <label class="form-label"><?= qh_h(qh_t('Doctor', 'الطبيب')) ?></label>
<select class="form-select js-doctor-select" name="doctor_id" required> <select class="form-select js-doctor-select" name="doctor_id" required>
<option value="">Choose doctor</option> <option value=""><?= qh_h(qh_t('Choose doctor', 'اختر الطبيب')) ?></option>
<?php foreach ($doctors as $doctor): ?> <?php foreach ($doctors as $doctor): ?>
<option value="<?= qh_h((string) $doctor['id']) ?>" data-clinic-id="<?= qh_h((string) $doctor['clinic_id']) ?>"> <option value="<?= qh_h((string) $doctor['id']) ?>" data-clinic-id="<?= qh_h((string) $doctor['clinic_id']) ?>"><?= qh_h(qh_name($doctor)) ?> · <?= qh_h(qh_t('Room', 'غرفة')) ?> <?= qh_h($doctor['room_number']) ?></option>
<?= qh_h($doctor['name_en']) ?> / <?= qh_h($doctor['name_ar']) ?> · Room <?= qh_h($doctor['room_number']) ?>
</option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
<div> <div>
<label class="form-label">Preferred language / اللغة المفضلة</label> <label class="form-label"><?= qh_h(qh_t('Preferred language', 'اللغة المفضلة')) ?></label>
<select class="form-select" name="language_pref"> <select class="form-select" name="language_pref">
<option value="en">English</option> <option value="en">English</option>
<option value="ar">العربية</option> <option value="ar">العربية</option>
</select> </select>
</div> </div>
<button class="btn btn-dark" type="submit">Issue ticket / إصدار التذكرة</button> <button class="btn btn-dark" type="submit"><?= qh_h(qh_t('Issue ticket', 'إصدار التذكرة')) ?></button>
</form> </form>
</div> </div>
</div> </div>
@ -66,40 +69,36 @@ qh_page_start('reception', 'Reception ticket issuance', 'Reception desk ticket i
<div class="panel-card ticket-card mb-4"> <div class="panel-card ticket-card mb-4">
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap"> <div class="d-flex justify-content-between align-items-start gap-3 flex-wrap">
<div> <div>
<div class="section-kicker">Issued ticket / التذكرة الصادرة</div> <div class="section-kicker"><?= qh_h(qh_t('Issued ticket', 'التذكرة الصادرة')) ?></div>
<div class="ticket-number mt-2"><?= qh_h($currentTicket['ticket_number']) ?></div> <div class="ticket-number mt-2"><?= qh_h($currentTicket['ticket_number']) ?></div>
<div class="mt-2 fw-semibold"><?= qh_h($currentTicket['patient_name']) ?></div> <div class="mt-2 fw-semibold"><?= qh_h($currentTicket['patient_name']) ?></div>
<div class="text-secondary"><?= qh_h($currentTicket['clinic_name_en'] ?? '') ?> · <?= qh_h($currentTicket['doctor_name_en'] ?? '') ?> · Room <?= qh_h($currentTicket['doctor_room'] ?? '--') ?></div> <div class="text-secondary"><?= qh_h(qh_name($currentTicket, 'clinic_name')) ?> · <?= qh_h(qh_name($currentTicket, 'doctor_name')) ?> · <?= qh_h(qh_t('Room', 'غرفة')) ?> <?= qh_h($currentTicket['doctor_room'] ?? '--') ?></div>
</div> </div>
<div class="d-flex flex-column align-items-lg-end gap-2"> <div class="d-flex flex-column align-items-lg-end gap-2">
<?= qh_status_badge($currentTicket['status']) ?> <?= qh_status_badge($currentTicket['status']) ?>
<button class="btn btn-outline-dark btn-sm js-print-ticket" type="button">Print ticket</button> <button class="btn btn-outline-dark btn-sm js-print-ticket" type="button"><?= qh_h(qh_t('Print ticket', 'طباعة التذكرة')) ?></button>
</div> </div>
</div> </div>
<hr> <hr>
<div class="row g-3 small"> <div class="row g-3 small">
<div class="col-md-4"><strong>Issued:</strong><br><?= qh_format_datetime($currentTicket['created_at']) ?></div> <div class="col-md-4"><strong><?= qh_h(qh_t('Issued', 'وقت الإصدار')) ?>:</strong><br><?= qh_format_datetime($currentTicket['created_at']) ?></div>
<div class="col-md-4"><strong>Language:</strong><br><?= strtoupper(qh_h($currentTicket['language_pref'])) ?></div> <div class="col-md-4"><strong><?= qh_h(qh_t('Language', 'اللغة')) ?>:</strong><br><?= qh_h(qh_locale_label($currentTicket['language_pref'] ?? 'en')) ?></div>
<div class="col-md-4"><strong>Next stop:</strong><br><?= (int) $currentTicket['clinic_requires_vitals'] === 1 ? 'Nursing vitals / التمريض' : 'Doctor waiting / انتظار الطبيب' ?></div> <div class="col-md-4"><strong><?= qh_h(qh_t('Next stop', 'المحطة التالية')) ?>:</strong><br><?= qh_h(qh_ticket_next_stop($currentTicket)) ?></div>
</div> </div>
</div> </div>
<?php endif; ?> <?php endif; ?>
<div class="panel-card h-100"> <div class="panel-card h-100">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3"> <h2 class="section-title mb-1"><?= qh_h(qh_t('Todays tickets', 'تذاكر اليوم')) ?></h2>
<div> <p class="section-copy mb-3"><?= qh_h(qh_t('Latest tickets and where they currently sit in the visit flow.', 'أحدث التذاكر وموقعها الحالي في مسار الزيارة.')) ?></p>
<h2 class="section-title mb-1">Todays tickets / تذاكر اليوم</h2>
<p class="section-copy mb-0">The latest issued tickets and where they currently are in the visit flow.</p>
</div>
</div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table align-middle mb-0"> <table class="table align-middle mb-0">
<thead> <thead>
<tr> <tr>
<th>Ticket</th> <th><?= qh_h(qh_t('Ticket', 'التذكرة')) ?></th>
<th>Patient</th> <th><?= qh_h(qh_t('Patient', 'المريض')) ?></th>
<th>Clinic</th> <th><?= qh_h(qh_t('Clinic', 'العيادة')) ?></th>
<th>Status</th> <th><?= qh_h(qh_t('Status', 'الحالة')) ?></th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -108,9 +107,9 @@ qh_page_start('reception', 'Reception ticket issuance', 'Reception desk ticket i
<tr> <tr>
<td class="fw-semibold"><?= qh_h($ticket['ticket_number']) ?></td> <td class="fw-semibold"><?= qh_h($ticket['ticket_number']) ?></td>
<td><?= qh_h($ticket['patient_name']) ?></td> <td><?= qh_h($ticket['patient_name']) ?></td>
<td><?= qh_h($ticket['clinic_name_en'] ?? '—') ?></td> <td><?= qh_h(qh_name($ticket, 'clinic_name')) ?></td>
<td><?= qh_status_badge($ticket['status']) ?></td> <td><?= qh_status_badge($ticket['status']) ?></td>
<td class="text-end"><a class="btn btn-sm btn-outline-dark" href="ticket.php?id=<?= qh_h((string) $ticket['id']) ?>">View</a></td> <td class="text-end"><a class="btn btn-sm btn-outline-dark" href="<?= qh_h(qh_url('ticket.php', ['id' => (int) $ticket['id']])) ?>"><?= qh_h(qh_t('View', 'عرض')) ?></a></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>

View File

@ -6,22 +6,27 @@ qh_boot();
$ticketId = isset($_GET['id']) ? (int) $_GET['id'] : 0; $ticketId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
$ticket = $ticketId > 0 ? qh_fetch_ticket($ticketId) : null; $ticket = $ticketId > 0 ? qh_fetch_ticket($ticketId) : null;
qh_page_start('home', 'Ticket detail', 'Detailed patient ticket timeline for the hospital queue workflow.'); qh_page_start(
'home',
qh_t('Ticket detail', 'تفاصيل التذكرة'),
qh_t('Detailed patient ticket timeline for the hospital queue workflow.', 'عرض تفصيلي لخط سير التذكرة في مسار طابور المستشفى.')
);
?> ?>
<div class="container-lg px-3 px-lg-4"> <div class="container-lg px-3 px-lg-4">
<section class="page-header-panel mb-4"> <section class="page-header-panel mb-4">
<div> <div>
<span class="section-kicker">Ticket detail / تفاصيل التذكرة</span> <span class="section-kicker"><?= qh_h(qh_t('Ticket detail', 'تفاصيل التذكرة')) ?></span>
<h1 class="section-title-xl mt-2">Track one patient through the visit.</h1> <div class="locale-chip mt-3"><?= qh_h(qh_current_language_badge()) ?></div>
<p class="section-copy mb-0">This view confirms the assigned clinic, doctor, room, vitals notes, and current status.</p> <h1 class="section-title-xl mt-2"><?= qh_h(qh_t('Track one patient through the visit.', 'تابع مريضاً واحداً خلال الزيارة.')) ?></h1>
<p class="section-copy mb-0"><?= qh_h(qh_t('Review the assigned clinic, doctor, room, vitals notes, and current status.', 'راجع العيادة والطبيب والغرفة وملاحظات العلامات الحيوية والحالة الحالية.')) ?></p>
</div> </div>
</section> </section>
<?php if (!$ticket): ?> <?php if (!$ticket): ?>
<div class="panel-card"> <div class="panel-card">
<div class="empty-state"> <div class="empty-state">
<strong>Ticket not found.</strong> <strong><?= qh_h(qh_t('Ticket not found.', 'لم يتم العثور على التذكرة.')) ?></strong>
<span>Return to reception and choose a valid ticket.</span> <span><?= qh_h(qh_t('Return to reception and choose a valid ticket.', 'عد إلى الاستقبال واختر تذكرة صالحة.')) ?></span>
</div> </div>
</div> </div>
<?php else: ?> <?php else: ?>
@ -30,11 +35,11 @@ qh_page_start('home', 'Ticket detail', 'Detailed patient ticket timeline for the
<div> <div>
<div class="ticket-number"><?= qh_h($ticket['ticket_number']) ?></div> <div class="ticket-number"><?= qh_h($ticket['ticket_number']) ?></div>
<div class="mt-2 fw-semibold"><?= qh_h($ticket['patient_name']) ?></div> <div class="mt-2 fw-semibold"><?= qh_h($ticket['patient_name']) ?></div>
<div class="text-secondary"><?= qh_h($ticket['clinic_name_en'] ?? '') ?> · <?= qh_h($ticket['doctor_name_en'] ?? '') ?> · Room <?= qh_h($ticket['doctor_room'] ?? '--') ?></div> <div class="text-secondary"><?= qh_h(qh_name($ticket, 'clinic_name')) ?> · <?= qh_h(qh_name($ticket, 'doctor_name')) ?> · <?= qh_h(qh_t('Room', 'غرفة')) ?> <?= qh_h($ticket['doctor_room'] ?? '--') ?></div>
</div> </div>
<div class="d-flex flex-column gap-2 align-items-lg-end"> <div class="d-flex flex-column gap-2 align-items-lg-end">
<?= qh_status_badge($ticket['status']) ?> <?= qh_status_badge($ticket['status']) ?>
<span class="small text-secondary">Preferred language: <?= strtoupper(qh_h($ticket['language_pref'])) ?></span> <span class="small text-secondary"><?= qh_h(qh_t('Preferred language', 'اللغة المفضلة')) ?>: <?= qh_h(qh_locale_label($ticket['language_pref'] ?? 'en')) ?></span>
</div> </div>
</div> </div>
</div> </div>
@ -42,62 +47,32 @@ qh_page_start('home', 'Ticket detail', 'Detailed patient ticket timeline for the
<div class="row g-4"> <div class="row g-4">
<div class="col-lg-7"> <div class="col-lg-7">
<div class="panel-card h-100"> <div class="panel-card h-100">
<h2 class="section-title mb-3">Visit timeline / خط سير الزيارة</h2> <h2 class="section-title mb-3"><?= qh_h(qh_t('Visit timeline', 'خط سير الزيارة')) ?></h2>
<div class="timeline-list"> <div class="timeline-list">
<div class="timeline-item done"> <div class="timeline-item done"><div class="timeline-dot"></div><div><div class="fw-semibold"><?= qh_h(qh_t('Ticket issued', 'تم إصدار التذكرة')) ?></div><div class="small text-secondary"><?= qh_format_datetime($ticket['created_at']) ?></div></div></div>
<div class="timeline-dot"></div>
<div>
<div class="fw-semibold">Ticket issued / تم إصدار التذكرة</div>
<div class="small text-secondary"><?= qh_format_datetime($ticket['created_at']) ?></div>
</div>
</div>
<?php if ((int) $ticket['clinic_requires_vitals'] === 1): ?> <?php if ((int) $ticket['clinic_requires_vitals'] === 1): ?>
<div class="timeline-item <?= in_array($ticket['status'], ['ready_for_doctor', 'called', 'in_progress', 'done', 'no_show'], true) ? 'done' : 'current' ?>"> <div class="timeline-item <?= in_array($ticket['status'], ['ready_for_doctor', 'called', 'in_progress', 'done', 'no_show'], true) ? 'done' : 'current' ?>"><div class="timeline-dot"></div><div><div class="fw-semibold"><?= qh_h(qh_t('Nursing vitals', 'العلامات الحيوية')) ?></div><div class="small text-secondary"><?= qh_h($ticket['vitals_notes'] ?: qh_t('Waiting for nursing input.', 'بانتظار إدخال التمريض.')) ?></div></div></div>
<div class="timeline-dot"></div>
<div>
<div class="fw-semibold">Nursing vitals / العلامات الحيوية</div>
<div class="small text-secondary"><?= qh_h($ticket['vitals_notes'] ?: 'Waiting for nursing input.') ?></div>
</div>
</div>
<?php endif; ?> <?php endif; ?>
<div class="timeline-item <?= in_array($ticket['status'], ['called', 'in_progress', 'done', 'no_show'], true) ? 'done' : 'current' ?>"> <div class="timeline-item <?= in_array($ticket['status'], ['called', 'in_progress', 'done', 'no_show'], true) ? 'done' : 'current' ?>"><div class="timeline-dot"></div><div><div class="fw-semibold"><?= qh_h(qh_t('Ready for doctor', 'جاهز للطبيب')) ?></div><div class="small text-secondary"><?= qh_h(qh_t('Assigned to', 'مخصص إلى')) ?> <?= qh_h(qh_name($ticket, 'doctor_name', qh_t('Doctor', 'الطبيب'))) ?>، <?= qh_h(qh_t('room', 'الغرفة')) ?> <?= qh_h($ticket['doctor_room'] ?? '--') ?></div></div></div>
<div class="timeline-dot"></div> <div class="timeline-item <?= in_array($ticket['status'], ['in_progress', 'done', 'no_show'], true) ? 'done' : (($ticket['status'] === 'called') ? 'current' : '') ?>"><div class="timeline-dot"></div><div><div class="fw-semibold"><?= qh_h(qh_t('Called to room', 'تم النداء للغرفة')) ?></div><div class="small text-secondary"><?= qh_format_datetime($ticket['called_at']) ?></div></div></div>
<div> <div class="timeline-item <?= in_array($ticket['status'], ['done', 'no_show'], true) ? 'done' : '' ?>"><div class="timeline-dot"></div><div><div class="fw-semibold"><?= qh_h(qh_t('Visit closed', 'إغلاق الزيارة')) ?></div><div class="small text-secondary"><?= qh_h($ticket['status'] === 'done' ? qh_t('Completed successfully.', 'تمت الزيارة بنجاح.') : ($ticket['status'] === 'no_show' ? qh_t('Marked as no-show.', 'تم تسجيل عدم الحضور.') : qh_t('Still active.', 'لا تزال نشطة.'))) ?></div></div></div>
<div class="fw-semibold">Ready for doctor / جاهز للطبيب</div>
<div class="small text-secondary">Assigned to <?= qh_h($ticket['doctor_name_en'] ?? 'Doctor') ?>, room <?= qh_h($ticket['doctor_room'] ?? '--') ?></div>
</div>
</div>
<div class="timeline-item <?= in_array($ticket['status'], ['in_progress', 'done', 'no_show'], true) ? 'done' : (($ticket['status'] === 'called') ? 'current' : '') ?>">
<div class="timeline-dot"></div>
<div>
<div class="fw-semibold">Called to room / تم النداء للغرفة</div>
<div class="small text-secondary"><?= qh_format_datetime($ticket['called_at']) ?></div>
</div>
</div>
<div class="timeline-item <?= in_array($ticket['status'], ['done', 'no_show'], true) ? 'done' : '' ?>">
<div class="timeline-dot"></div>
<div>
<div class="fw-semibold">Visit closed / إغلاق الزيارة</div>
<div class="small text-secondary"><?= $ticket['status'] === 'done' ? 'Completed successfully.' : ($ticket['status'] === 'no_show' ? 'Marked as no-show.' : 'Still active.') ?></div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-5"> <div class="col-lg-5">
<div class="panel-card h-100"> <div class="panel-card h-100">
<h2 class="section-title mb-3">Details / التفاصيل</h2> <h2 class="section-title mb-3"><?= qh_h(qh_t('Details', 'التفاصيل')) ?></h2>
<dl class="detail-list mb-0"> <dl class="detail-list mb-0">
<div><dt>Clinic</dt><dd><?= qh_h($ticket['clinic_name_en'] ?? '—') ?></dd></div> <div><dt><?= qh_h(qh_t('Clinic', 'العيادة')) ?></dt><dd><?= qh_h(qh_name($ticket, 'clinic_name')) ?></dd></div>
<div><dt>Doctor</dt><dd><?= qh_h($ticket['doctor_name_en'] ?? '—') ?></dd></div> <div><dt><?= qh_h(qh_t('Doctor', 'الطبيب')) ?></dt><dd><?= qh_h(qh_name($ticket, 'doctor_name')) ?></dd></div>
<div><dt>Room</dt><dd><?= qh_h($ticket['doctor_room'] ?? '--') ?></dd></div> <div><dt><?= qh_h(qh_t('Room', 'الغرفة')) ?></dt><dd><?= qh_h($ticket['doctor_room'] ?? '--') ?></dd></div>
<div><dt>Vitals note</dt><dd><?= qh_h($ticket['vitals_notes'] ?: 'Not captured yet.') ?></dd></div> <div><dt><?= qh_h(qh_t('Vitals note', 'ملاحظة العلامات')) ?></dt><dd><?= qh_h($ticket['vitals_notes'] ?: qh_t('Not captured yet.', 'لم تُسجل بعد.')) ?></dd></div>
<div><dt>Last note</dt><dd><?= qh_h($ticket['display_note'] ?: '—') ?></dd></div> <div><dt><?= qh_h(qh_t('Last note', 'آخر ملاحظة')) ?></dt><dd><?= qh_h(qh_ticket_last_note($ticket)) ?></dd></div>
</dl> </dl>
<div class="d-flex flex-wrap gap-2 mt-4"> <div class="d-flex flex-wrap gap-2 mt-4">
<a class="btn btn-outline-dark btn-sm" href="reception.php?ticket_id=<?= qh_h((string) $ticket['id']) ?>">Back to reception</a> <a class="btn btn-outline-dark btn-sm" href="<?= qh_h(qh_url('reception.php', ['ticket_id' => (int) $ticket['id']])) ?>"><?= qh_h(qh_t('Back to reception', 'العودة إلى الاستقبال')) ?></a>
<a class="btn btn-outline-dark btn-sm" href="doctor.php?doctor_id=<?= qh_h((string) ($ticket['doctor_id'] ?? 0)) ?>">Open doctor queue</a> <a class="btn btn-outline-dark btn-sm" href="<?= qh_h(qh_url('doctor.php', ['doctor_id' => (int) ($ticket['doctor_id'] ?? 0)])) ?>"><?= qh_h(qh_t('Open doctor queue', 'فتح طابور الطبيب')) ?></a>
<a class="btn btn-outline-dark btn-sm" href="display.php">Open display</a> <a class="btn btn-outline-dark btn-sm" href="<?= qh_h(qh_url('display.php')) ?>"><?= qh_h(qh_t('Open display', 'فتح الشاشة')) ?></a>
</div> </div>
</div> </div>
</div> </div>