Compare commits

..

26 Commits

Author SHA1 Message Date
Flatlogic Bot
1148add8f4 updating printing ticket 2026-04-02 17:35:51 +00:00
Flatlogic Bot
9991d51541 change display 2026-04-02 17:14:46 +00:00
Flatlogic Bot
22af275489 update display 2026-04-02 15:38:36 +00:00
Flatlogic Bot
d2da4004c3 update css for display 2026-04-02 15:30:48 +00:00
Flatlogic Bot
a82cf57175 update speech 2026-04-02 13:56:50 +00:00
Flatlogic Bot
d9c3b766ff update css 2026-04-02 13:19:22 +00:00
Flatlogic Bot
9dff4a2e24 save nursing 2026-04-02 12:53:32 +00:00
Flatlogic Bot
7b06146e61 modify ads 2026-04-02 12:35:28 +00:00
Flatlogic Bot
47f6bb2062 update display 2026-04-02 11:35:25 +00:00
Flatlogic Bot
b70adcd295 Autosave: 20260402-112504 2026-04-02 11:25:04 +00:00
Flatlogic Bot
e8e0abdac5 maximise display 2026-04-02 07:29:40 +00:00
Flatlogic Bot
5f6b8289df update sound 2026-04-01 10:05:01 +00:00
Flatlogic Bot
f81da63413 update voice 2026-04-01 09:38:33 +00:00
Flatlogic Bot
88ba3bb786 update istall 2026-04-01 09:16:10 +00:00
Flatlogic Bot
197c4b56f7 update display 2026-04-01 08:23:33 +00:00
Flatlogic Bot
8d5d2b0838 Autosave: 20260401-074438 2026-04-01 07:44:38 +00:00
Flatlogic Bot
2808b801bb update login 2026-04-01 04:41:54 +00:00
Flatlogic Bot
027212dca4 update printing 2026-04-01 04:27:17 +00:00
Flatlogic Bot
4fb5c927e3 update install 2026-04-01 04:12:16 +00:00
Flatlogic Bot
a232fc60a3 Autosave: 20260401-035926 2026-04-01 03:59:27 +00:00
Flatlogic Bot
a2f276f05e install file 2026-04-01 03:29:17 +00:00
Flatlogic Bot
e2985ac197 adding timezone 2026-04-01 01:50:29 +00:00
Flatlogic Bot
1fb5dec73f Autosave: 20260401-005145 2026-04-01 00:51:46 +00:00
Flatlogic Bot
9ef45e15f2 Autosave: 20260331-163419 2026-03-31 16:34:19 +00:00
Flatlogic Bot
0c7ba978a8 Autosave: 20260331-103916 2026-03-31 10:39:17 +00:00
Flatlogic Bot
fd3feb7878 updating display 2026-03-31 09:50:11 +00:00
29 changed files with 5759 additions and 508 deletions

153
admin.php Normal file
View File

@ -0,0 +1,153 @@
<?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();
$recentClinics = array_slice($clinics, 0, 4);
$recentDoctors = array_slice($doctors, 0, 5);
$profile = qh_fetch_hospital_profile();
qh_page_start(
'admin',
qh_t('Admin overview', 'نظرة عامة للإدارة'),
qh_t('Structured admin overview with separate pages for clinics and doctors.', 'نظرة عامة منظمة للإدارة مع صفحات مستقلة للعيادات والأطباء.')
);
?>
<div class="container-fluid container-xxl px-3 px-lg-4">
<div class="admin-layout">
<aside class="admin-sidebar-column">
<?php qh_render_admin_sidebar('admin.php', $stats); ?>
</aside>
<div class="admin-content-stack">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0 text-gray-800 fw-bold"><?= qh_h(qh_t('Admin Dashboard', 'لوحة تحكم الإدارة')) ?></h1>
<p class="text-muted mb-0 mt-1"><?= qh_h(qh_t('System configuration and management.', 'إعدادات وإدارة النظام.')) ?></p>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-md-3 col-sm-6">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1 small"><?= qh_h(qh_t('Clinics', 'العيادات')) ?></div>
<div class="h3 mb-0 font-weight-bold text-gray-800"><?= qh_h((string) $stats['clinics']) ?></div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1 small"><?= qh_h(qh_t('Doctors', 'الأطباء')) ?></div>
<div class="h3 mb-0 font-weight-bold text-gray-800"><?= qh_h((string) $stats['doctors']) ?></div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1 small"><?= qh_h(qh_t('Vitals Clinics', 'عيادات العلامات')) ?></div>
<div class="h3 mb-0 font-weight-bold text-gray-800"><?= qh_h((string) $stats['vitals_clinics']) ?></div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1 small"><?= qh_h(qh_t('Direct Clinics', 'عيادات مباشرة')) ?></div>
<div class="h3 mb-0 font-weight-bold text-gray-800"><?= qh_h((string) $stats['direct_clinics']) ?></div>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-lg-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Hospital Profile', 'ملف المستشفى')) ?></h5>
<a class="btn btn-sm btn-outline-secondary shadow-sm" href="<?= qh_h(qh_url('admin_hospital.php')) ?>"><?= qh_h(qh_t('Edit', 'تعديل')) ?></a>
</div>
<div class="card-body">
<div class="d-flex flex-column gap-3">
<div>
<div class="fw-semibold text-dark"><?= qh_h(qh_hospital_name()) ?></div>
<div class="small text-muted"><?= qh_h((string) ($profile['short_name'] ?? qh_t('No short name', 'لا يوجد اسم مختصر'))) ?></div>
</div>
<div>
<div class="small text-muted mb-1"><?= qh_h(qh_t('Phone', 'الهاتف')) ?></div>
<div class="fw-semibold text-dark"><?= qh_h((string) (($profile['phone'] ?? '') !== '' ? $profile['phone'] : '--')) ?></div>
</div>
<div>
<div class="small text-muted mb-1"><?= qh_h(qh_t('Working hours', 'ساعات العمل')) ?></div>
<div class="fw-semibold text-dark"><?= qh_h(qh_name($profile, 'working_hours', '--')) ?></div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Recent Clinics', 'أحدث العيادات')) ?></h5>
<a class="btn btn-sm btn-outline-secondary shadow-sm" href="<?= qh_h(qh_url('admin_clinics.php')) ?>"><?= qh_h(qh_t('Manage', 'إدارة')) ?></a>
</div>
<div class="card-body p-0">
<?php if ($recentClinics === []): ?>
<div class="text-center py-4 text-muted"><p class="mb-0 small"><?= qh_h(qh_t('No clinics yet.', 'لا توجد عيادات.')) ?></p></div>
<?php else: ?>
<div class="list-group list-group-flush">
<?php foreach ($recentClinics as $clinic): ?>
<div class="list-group-item d-flex justify-content-between align-items-center py-3 border-0 border-bottom">
<div>
<div class="fw-semibold text-dark mb-1"><?= qh_h(qh_name($clinic)) ?></div>
<div class="small text-muted"><?= qh_h(qh_t('Code', 'الرمز')) ?>: <?= qh_h((string) $clinic['code']) ?></div>
</div>
<span class="badge bg-<?= (int) $clinic['requires_vitals'] === 1 ? 'warning text-dark' : 'info text-dark' ?> rounded-pill px-2 py-1">
<?= qh_h((int) $clinic['requires_vitals'] === 1 ? qh_t('Vitals', 'علامات') : qh_t('Direct', 'مباشر')) ?>
</span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Recent Doctors', 'أحدث الأطباء')) ?></h5>
<a class="btn btn-sm btn-outline-secondary shadow-sm" href="<?= qh_h(qh_url('admin_doctors.php')) ?>"><?= qh_h(qh_t('Manage', 'إدارة')) ?></a>
</div>
<div class="card-body p-0">
<?php if ($recentDoctors === []): ?>
<div class="text-center py-4 text-muted"><p class="mb-0 small"><?= qh_h(qh_t('No doctors yet.', 'لا يوجد أطباء.')) ?></p></div>
<?php else: ?>
<div class="list-group list-group-flush">
<?php foreach ($recentDoctors as $doctor): ?>
<div class="list-group-item d-flex justify-content-between align-items-center py-3 border-0 border-bottom">
<div>
<div class="fw-semibold text-dark mb-1"><?= qh_h(qh_name($doctor)) ?></div>
<div class="small text-muted"><?= qh_h(qh_name($doctor, 'clinic_name', qh_t('Unassigned', 'غير محدد'))) ?></div>
</div>
<span class="badge bg-light text-dark border px-2 py-1"><?= qh_h(qh_t('Rm', 'غرفة')) ?> <?= qh_h((string) $doctor['room_number']) ?></span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<?php qh_page_end(); ?>

303
admin_ads.php Normal file
View File

@ -0,0 +1,303 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/queue_bootstrap.php';
qh_boot();
qh_admin_handle_request();
// Ensure the table exists
$pdo = db();
$pdo->exec("CREATE TABLE IF NOT EXISTS hospital_ads (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NULL,
video_path VARCHAR(255) NOT NULL,
is_active TINYINT(1) DEFAULT 1,
sort_order INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
$pdo->exec("CREATE TABLE IF NOT EXISTS hospital_news (
id INT AUTO_INCREMENT PRIMARY KEY,
phrase VARCHAR(1000) NOT NULL,
is_active TINYINT(1) DEFAULT 1,
sort_order INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
// Handle Form Submissions
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['action']) && $_POST['action'] === 'add_video' && !empty($_FILES['video']['name'])) {
$uploadDir = __DIR__ . '/assets/videos/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
$ext = strtolower(pathinfo($_FILES['video']['name'], PATHINFO_EXTENSION));
$allowed = ['mp4', 'webm', 'ogg'];
if (!in_array($ext, $allowed)) {
$message = qh_t("Invalid video format (.$ext). Allowed formats: MP4, WebM, OGG.", "تنسيق فيديو غير صالح (.$ext). الصيغ المسموحة: MP4, WebM, OGG.");
} elseif ($_FILES['video']['error'] !== UPLOAD_ERR_OK) {
$errorMap = [
UPLOAD_ERR_INI_SIZE => 'File exceeds max size in php.ini.',
UPLOAD_ERR_FORM_SIZE => 'File exceeds max size in HTML form.',
UPLOAD_ERR_PARTIAL => 'File was partially uploaded.',
UPLOAD_ERR_NO_FILE => 'No file was uploaded.',
UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder.',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.',
UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the upload.'
];
$errMsg = $errorMap[$_FILES['video']['error']] ?? 'Unknown error';
$message = qh_t('Upload error: ' . $errMsg, 'خطأ في الرفع: ' . $errMsg);
} else {
$filename = uniqid('vid_') . '.' . $ext;
$dest = $uploadDir . $filename;
if (move_uploaded_file($_FILES['video']['tmp_name'], $dest)) {
$title = $_POST['title'] ?? '';
$path = 'assets/videos/uploads/' . $filename;
$stmt = $pdo->prepare("INSERT INTO hospital_ads (title, video_path, is_active) VALUES (?, ?, 1)");
$stmt->execute([$title, $path]);
$message = qh_t('Video uploaded successfully.', 'تم رفع الفيديو بنجاح.');
} else {
$message = qh_t('Failed to save uploaded file.', 'فشل في حفظ الملف المرفوع.');
}
}
} elseif (isset($_POST['action']) && $_POST['action'] === 'delete_video' && !empty($_POST['video_id'])) {
$id = (int) $_POST['video_id'];
$stmt = $pdo->prepare("SELECT video_path FROM hospital_ads WHERE id = ?");
$stmt->execute([$id]);
$video = $stmt->fetch();
if ($video) {
$fullPath = __DIR__ . '/' . $video['video_path'];
if (file_exists($fullPath)) {
unlink($fullPath);
}
$pdo->prepare("DELETE FROM hospital_ads WHERE id = ?")->execute([$id]);
$message = qh_t('Video deleted successfully.', 'تم حذف الفيديو بنجاح.');
}
} elseif (isset($_POST['action']) && $_POST['action'] === 'toggle_status' && !empty($_POST['video_id'])) {
$id = (int) $_POST['video_id'];
$stmt = $pdo->prepare("UPDATE hospital_ads SET is_active = NOT is_active WHERE id = ?");
$stmt->execute([$id]);
$message = qh_t('Video status updated.', 'تم تحديث حالة الفيديو.');
} elseif (isset($_POST['action']) && $_POST['action'] === 'add_news' && !empty($_POST['phrase'])) {
$phrase = trim($_POST['phrase']);
$stmt = $pdo->prepare("INSERT INTO hospital_news (phrase, is_active) VALUES (?, 1)");
$stmt->execute([$phrase]);
$message = qh_t('Phrase added successfully.', 'تمت إضافة العبارة بنجاح.');
} elseif (isset($_POST['action']) && $_POST['action'] === 'delete_news' && !empty($_POST['news_id'])) {
$id = (int) $_POST['news_id'];
$pdo->prepare("DELETE FROM hospital_news WHERE id = ?")->execute([$id]);
$message = qh_t('Phrase deleted successfully.', 'تم حذف العبارة بنجاح.');
} elseif (isset($_POST['action']) && $_POST['action'] === 'toggle_news_status' && !empty($_POST['news_id'])) {
$id = (int) $_POST['news_id'];
$pdo->prepare("UPDATE hospital_news SET is_active = NOT is_active WHERE id = ?")->execute([$id]);
$message = qh_t('Phrase status updated.', 'تم تحديث حالة العبارة.');
}
}
// Fetch existing ads
$stmt = $pdo->query("SELECT * FROM hospital_ads ORDER BY sort_order ASC, id DESC");
$ads = $stmt->fetchAll();
$stmt2 = $pdo->query("SELECT * FROM hospital_news ORDER BY sort_order ASC, id DESC");
$newsPhrases = $stmt2->fetchAll();
qh_page_start(
'admin_ads',
qh_t('Manage Advertisements', 'إدارة الإعلانات'),
qh_t('Upload and manage videos to display on the queue screen.', 'رفع وإدارة الفيديوهات لعرضها على شاشة الطابور.')
);
?>
<div class="container-fluid container-xxl px-3 px-lg-4">
<div class="admin-layout">
<aside class="admin-sidebar-column">
<?php qh_render_admin_sidebar('admin_ads.php'); ?>
</aside>
<div class="admin-content-stack">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0 text-gray-800 fw-bold"><?= qh_h(qh_t('Advertisements (Videos)', 'الإعلانات (فيديوهات)')) ?></h1>
<p class="text-muted mb-0 mt-1"><?= qh_h(qh_t('These videos will be played sequentially on the display screen.', 'سيتم تشغيل هذه الفيديوهات بالتتابع على شاشة العرض.')) ?></p>
</div>
</div>
<?php if ($message): ?>
<div class="alert alert-info shadow-sm"><?= qh_h($message) ?></div>
<?php endif; ?>
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom py-3">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Upload New Video', 'رفع فيديو جديد')) ?></h5>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data" class="row g-3">
<input type="hidden" name="action" value="add_video">
<div class="col-md-6">
<label class="form-label fw-semibold text-dark"><?= qh_h(qh_t('Video Title (Optional)', 'عنوان الفيديو (اختياري)')) ?></label>
<input type="text" name="title" class="form-control" placeholder="<?= qh_h(qh_t('e.g. Summer Promo', 'مثال: عرض الصيف')) ?>">
</div>
<div class="col-md-6">
<label class="form-label fw-semibold text-dark"><?= qh_h(qh_t('Video File', 'ملف الفيديو')) ?></label>
<input type="file" name="video" class="form-control" accept="video/mp4,video/webm,video/ogg" required>
<div class="form-text"><?= qh_h(qh_t('Formats: MP4, WebM, OGG.', 'الصيغ: MP4, WebM, OGG.')) ?></div>
</div>
<div class="col-12 mt-3">
<button type="submit" class="btn btn-primary px-4"><?= qh_h(qh_t('Upload Video', 'رفع الفيديو')) ?></button>
</div>
</form>
</div>
</div>
<div class="card shadow-sm border-0">
<div class="card-header bg-white border-bottom py-3">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Uploaded Videos', 'الفيديوهات المرفوعة')) ?></h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="border-0 px-4 py-3"><?= qh_h(qh_t('Preview', 'معاينة')) ?></th>
<th class="border-0 py-3"><?= qh_h(qh_t('Title', 'العنوان')) ?></th>
<th class="border-0 py-3 text-center"><?= qh_h(qh_t('Status', 'الحالة')) ?></th>
<th class="border-0 px-4 py-3 text-end"><?= qh_h(qh_t('Actions', 'الإجراءات')) ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($ads)): ?>
<tr>
<td colspan="4" class="text-center py-4 text-muted"><?= qh_h(qh_t('No videos uploaded yet.', 'لم يتم رفع فيديوهات بعد.')) ?></td>
</tr>
<?php else: ?>
<?php foreach ($ads as $ad): ?>
<tr>
<td class="px-4 py-3">
<video width="120" class="rounded shadow-sm border" muted>
<source src="<?= qh_h($ad['video_path']) ?>" type="video/mp4">
</video>
</td>
<td class="py-3 fw-semibold text-dark">
<?= qh_h($ad['title'] ?: qh_t('Untitled', 'بدون عنوان')) ?>
</td>
<td class="py-3 text-center">
<?php if ($ad['is_active']): ?>
<span class="badge bg-success rounded-pill px-3"><?= qh_h(qh_t('Active', 'مفعل')) ?></span>
<?php else: ?>
<span class="badge bg-secondary rounded-pill px-3"><?= qh_h(qh_t('Inactive', 'غير مفعل')) ?></span>
<?php endif; ?>
</td>
<td class="px-4 py-3 text-end">
<div class="d-flex justify-content-end gap-2">
<form method="post" class="m-0 p-0">
<input type="hidden" name="action" value="toggle_status">
<input type="hidden" name="video_id" value="<?= (int) $ad['id'] ?>">
<button type="submit" class="btn btn-sm <?= $ad['is_active'] ? 'btn-outline-secondary' : 'btn-outline-success' ?>">
<?= qh_h($ad['is_active'] ? qh_t('Disable', 'تعطيل') : qh_t('Enable', 'تفعيل')) ?>
</button>
</form>
<form method="post" class="m-0 p-0" onsubmit="return confirm('<?= qh_h(qh_t('Are you sure you want to delete this video?', 'هل أنت متأكد أنك تريد حذف هذا الفيديو؟')) ?>');">
<input type="hidden" name="action" value="delete_video">
<input type="hidden" name="video_id" value="<?= (int) $ad['id'] ?>">
<button type="submit" class="btn btn-sm btn-outline-danger"><?= qh_h(qh_t('Delete', 'حذف')) ?></button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- News Ticker Section -->
<div class="d-flex justify-content-between align-items-center mb-4 mt-5">
<div>
<h1 class="h3 mb-0 text-gray-800 fw-bold"><?= qh_h(qh_t('News Ticker Phrases', 'عبارات شريط الأخبار')) ?></h1>
<p class="text-muted mb-0 mt-1"><?= qh_h(qh_t('These phrases will scroll at the bottom of the display screen.', 'ستتحرك هذه العبارات في أسفل شاشة العرض.')) ?></p>
</div>
</div>
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom py-3">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Add New Phrase', 'إضافة عبارة جديدة')) ?></h5>
</div>
<div class="card-body">
<form method="post" class="row g-3">
<input type="hidden" name="action" value="add_news">
<div class="col-md-10">
<label class="form-label fw-semibold text-dark"><?= qh_h(qh_t('Phrase Text', 'نص العبارة')) ?></label>
<input type="text" name="phrase" class="form-control" placeholder="<?= qh_h(qh_t('e.g. Welcome to our hospital. Please wait for your turn.', 'مثال: أهلاً بكم في مستشفانا. يرجى انتظار دوركم.')) ?>" required>
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100"><?= qh_h(qh_t('Add', 'إضافة')) ?></button>
</div>
</form>
</div>
</div>
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom py-3">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Ticker Phrases', 'عبارات الشريط')) ?></h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="border-0 px-4 py-3"><?= qh_h(qh_t('Phrase', 'العبارة')) ?></th>
<th class="border-0 py-3 text-center" style="width:120px;"><?= qh_h(qh_t('Status', 'الحالة')) ?></th>
<th class="border-0 px-4 py-3 text-end" style="width:200px;"><?= qh_h(qh_t('Actions', 'الإجراءات')) ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($newsPhrases)): ?>
<tr>
<td colspan="3" class="text-center py-4 text-muted"><?= qh_h(qh_t('No phrases added yet.', 'لم يتم إضافة عبارات بعد.')) ?></td>
</tr>
<?php else: ?>
<?php foreach ($newsPhrases as $n): ?>
<tr>
<td class="px-4 py-3 fw-semibold text-dark">
<?= qh_h($n['phrase']) ?>
</td>
<td class="py-3 text-center">
<?php if ($n['is_active']): ?>
<span class="badge bg-success rounded-pill px-3"><?= qh_h(qh_t('Active', 'مفعل')) ?></span>
<?php else: ?>
<span class="badge bg-secondary rounded-pill px-3"><?= qh_h(qh_t('Inactive', 'غير مفعل')) ?></span>
<?php endif; ?>
</td>
<td class="px-4 py-3 text-end">
<div class="d-flex justify-content-end gap-2">
<form method="post" class="m-0 p-0">
<input type="hidden" name="action" value="toggle_news_status">
<input type="hidden" name="news_id" value="<?= (int) $n['id'] ?>">
<button type="submit" class="btn btn-sm <?= $n['is_active'] ? 'btn-outline-secondary' : 'btn-outline-success' ?>">
<?= qh_h($n['is_active'] ? qh_t('Disable', 'تعطيل') : qh_t('Enable', 'تفعيل')) ?>
</button>
</form>
<form method="post" class="m-0 p-0" onsubmit="return confirm('<?= qh_h(qh_t('Are you sure you want to delete this phrase?', 'هل أنت متأكد أنك تريد حذف هذه العبارة؟')) ?>');">
<input type="hidden" name="action" value="delete_news">
<input type="hidden" name="news_id" value="<?= (int) $n['id'] ?>">
<button type="submit" class="btn btn-sm btn-outline-danger"><?= qh_h(qh_t('Delete', 'حذف')) ?></button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<?php qh_page_end(); ?>

185
admin_clinics.php Normal file
View File

@ -0,0 +1,185 @@
<?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-fluid 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">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0 text-gray-800 fw-bold"><?= qh_h(qh_t('Clinic Directory', 'دليل العيادات')) ?></h1>
<p class="text-muted mb-0 mt-1"><?= qh_h(qh_t('Manage clinics and routing rules.', 'إدارة العيادات وقواعد التوجيه.')) ?></p>
</div>
<?php if ($editClinic !== null): ?>
<a class="btn btn-primary shadow-sm" href="<?= qh_h(qh_url('admin_clinics.php')) ?>"><i class="bi bi-plus-lg me-1"></i><?= qh_h(qh_t('Add New', 'إضافة جديد')) ?></a>
<?php endif; ?>
</div>
<div class="card shadow-sm border-0 mb-4 bg-white">
<div class="card-body p-3">
<form method="get" class="d-flex gap-2 align-items-center flex-wrap" role="search">
<input type="hidden" name="lang" value="<?= qh_h(qh_locale()) ?>">
<div class="input-group">
<span class="input-group-text bg-light border-end-0"><i class="bi bi-search text-muted"></i></span>
<input id="clinicSearch" class="form-control border-start-0 ps-0 bg-light" type="search" name="q" value="<?= qh_h($search) ?>" placeholder="<?= qh_h(qh_t('Search by clinic name or code...', 'ابحث باسم العيادة أو الرمز...')) ?>">
<button class="btn btn-primary px-4" type="submit"><?= qh_h(qh_t('Search', 'بحث')) ?></button>
</div>
<?php if ($search !== ''): ?>
<a class="btn btn-outline-secondary bg-white" href="<?= qh_h(qh_url('admin_clinics.php')) ?>"><?= qh_h(qh_t('Clear', 'مسح')) ?></a>
<?php endif; ?>
</form>
</div>
</div>
<div class="row g-4">
<div class="col-xl-7 col-lg-6">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Clinics', 'العيادات')) ?></h5>
<span class="badge bg-light text-dark border px-2 py-1"><?= qh_h((string) count($clinics)) ?> <?= qh_h(qh_t('records', 'سجلات')) ?></span>
</div>
<div class="card-body p-0">
<?php if ($clinics === []): ?>
<div class="text-center py-5 text-muted">
<p class="mb-0"><?= qh_h(qh_t('No clinics found.', 'لم يتم العثور على عيادات.')) ?></p>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="border-0 px-4 py-3"><?= qh_h(qh_t('Code', 'الرمز')) ?></th>
<th class="border-0 py-3"><?= qh_h(qh_t('Clinic', 'العيادة')) ?></th>
<th class="border-0 py-3"><?= qh_h(qh_t('Routing', 'المسار')) ?></th>
<th class="border-0 px-4 py-3 text-end"><?= 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 class="px-4 py-3"><span class="badge bg-light text-dark border"><?= qh_h((string) $clinic['code']) ?></span></td>
<td class="py-3">
<div class="fw-semibold text-dark"><?= qh_h(qh_name($clinic)) ?></div>
<div class="small text-muted"><?= qh_h(qh_is_ar() ? (string) ($clinic['name_en'] ?? '') : (string) ($clinic['name_ar'] ?? '')) ?></div>
</td>
<td class="py-3"><span class="badge bg-<?= (int) $clinic['requires_vitals'] === 1 ? 'warning text-dark' : 'info text-dark' ?> rounded-pill px-2 py-1"><?= qh_h((int) $clinic['requires_vitals'] === 1 ? qh_t('Vitals', 'علامات') : qh_t('Direct', 'مباشر')) ?></span></td>
<td class="text-end px-4 py-3">
<div class="d-inline-flex gap-2">
<a class="btn btn-sm btn-light border shadow-sm" href="<?= qh_h($editUrl) ?>"><i class="bi bi-pencil me-1"></i><?= qh_h(qh_t('Edit', 'تعديل')) ?></a>
<form method="post" class="m-0" 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 shadow-sm bg-white" type="submit"><i class="bi bi-trash me-1"></i><?= qh_h(qh_t('Delete', 'حذف')) ?></button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-xl-5 col-lg-6">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white border-bottom py-3">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h($editClinic ? qh_t('Edit Clinic', 'تعديل العيادة') : qh_t('New Clinic', 'عيادة جديدة')) ?></h5>
</div>
<div class="card-body">
<form method="post" class="vstack gap-3">
<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-6">
<label class="form-label text-dark fw-semibold small" for="clinicCode"><?= qh_h(qh_t('Code', 'الرمز')) ?></label>
<input id="clinicCode" class="form-control bg-light" type="text" maxlength="10" name="code" value="<?= qh_h((string) ($editClinic['code'] ?? '')) ?>" required>
</div>
<div class="col-sm-6">
<label class="form-label text-dark fw-semibold small" for="clinicOrder"><?= qh_h(qh_t('Order', 'الترتيب')) ?></label>
<input id="clinicOrder" class="form-control bg-light" type="number" min="1" name="sort_order" value="<?= qh_h((string) ($editClinic['sort_order'] ?? 50)) ?>" required>
</div>
</div>
<div>
<label class="form-label text-dark fw-semibold small" for="clinicNameEn"><?= qh_h(qh_t('Name (English)', 'الاسم بالإنجليزية')) ?></label>
<input id="clinicNameEn" class="form-control bg-light" type="text" name="name_en" value="<?= qh_h((string) ($editClinic['name_en'] ?? '')) ?>" required>
</div>
<div>
<label class="form-label text-dark fw-semibold small" for="clinicNameAr"><?= qh_h(qh_t('Name (Arabic)', 'الاسم بالعربية')) ?></label>
<input id="clinicNameAr" class="form-control bg-light" type="text" name="name_ar" value="<?= qh_h((string) ($editClinic['name_ar'] ?? '')) ?>" required>
</div>
<div class="p-3 border rounded bg-light mt-2">
<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 ms-2 text-dark" for="clinicVitals"><?= qh_h(qh_t('Require vitals before doctor', 'يتطلب علامات حيوية')) ?></label>
</div>
</div>
<div class="d-flex gap-2 pt-3 border-top mt-2">
<button class="btn btn-primary shadow-sm" type="submit"><?= qh_h($editClinic ? qh_t('Save Changes', 'حفظ التعديلات') : qh_t('Add Clinic', 'إضافة عيادة')) ?></button>
<?php if ($editClinic): ?>
<a class="btn btn-outline-secondary bg-white shadow-sm" href="<?= qh_h(qh_url('admin_clinics.php')) ?>"><?= qh_h(qh_t('Cancel', 'إلغاء')) ?></a>
<?php endif; ?>
</div>
</form>
</div>
</div>
</div>
</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(); ?>

293
admin_hospital.php Normal file
View File

@ -0,0 +1,293 @@
<?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">
<form method="post" enctype="multipart/form-data" class="vstack gap-5">
<input type="hidden" name="action" value="save_hospital_profile">
<input type="hidden" name="return_to" value="admin_hospital.php">
<!-- Basic Info -->
<div>
<div class="admin-section-head mb-4">
<div>
<h2 class="section-title mb-1"><?= qh_h(qh_t('Basic Information', 'المعلومات الأساسية')) ?></h2>
<p class="section-copy mb-0"><?= qh_h(qh_t('Core naming used across the system.', 'التسمية الأساسية المستخدمة في النظام.')) ?></p>
</div>
</div>
<div class="row g-3">
<div class="col-lg-6">
<label class="form-label fw-bold" 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 fw-bold" 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 class="col-lg-4">
<label class="form-label fw-bold" 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 fw-bold" 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-4">
<label class="form-label fw-bold" 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 class="col-lg-6">
<label class="form-label fw-bold" for="newsTickerEn"><?= qh_h(qh_t('News Ticker (English)', 'شريط الأخبار بالإنجليزية')) ?></label>
<input id="newsTickerEn" class="form-control" type="text" name="news_ticker_en" value="<?= qh_h((string) ($profile['news_ticker_en'] ?? '')) ?>" placeholder="<?= qh_h(qh_t('Scrolling text for display board', 'نص متحرك لشاشة العرض')) ?>">
</div>
<div class="col-lg-6">
<label class="form-label fw-bold" for="newsTickerAr"><?= qh_h(qh_t('News Ticker (Arabic)', 'شريط الأخبار بالعربية')) ?></label>
<input id="newsTickerAr" class="form-control" type="text" name="news_ticker_ar" value="<?= qh_h((string) ($profile['news_ticker_ar'] ?? '')) ?>" placeholder="<?= qh_h(qh_t('Scrolling text for display board', 'نص متحرك لشاشة العرض')) ?>">
</div>
<div class="col-lg-6">
<label class="form-label fw-bold" for="appTimezone"><?= qh_h(qh_t('Timezone', 'المنطقة الزمنية')) ?></label>
<select id="appTimezone" class="form-select" name="timezone">
<?php foreach (timezone_identifiers_list() as $tz): ?>
<option value="<?= qh_h($tz) ?>" <?= ($profile['timezone'] ?? 'UTC') === $tz ? 'selected' : '' ?>><?= qh_h($tz) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-lg-6">
<label class="form-label fw-bold" for="appDefaultLanguage"><?= qh_h(qh_t('Default Language', 'اللغة الافتراضية')) ?></label>
<select id="appDefaultLanguage" class="form-select" name="default_language">
<option value="en" <?= ($profile['default_language'] ?? 'en') === 'en' ? 'selected' : '' ?>><?= qh_h(qh_t('English', 'الإنجليزية')) ?></option>
<option value="ar" <?= ($profile['default_language'] ?? 'en') === 'ar' ? 'selected' : '' ?>><?= qh_h(qh_t('Arabic', 'العربية')) ?></option>
</select>
</div>
</div>
</div>
<hr class="m-0 text-muted">
<!-- Contact & Location -->
<div>
<div class="admin-section-head mb-4">
<div>
<h2 class="section-title mb-1"><?= qh_h(qh_t('Contact & Location', 'التواصل والموقع')) ?></h2>
<p class="section-copy mb-0"><?= qh_h(qh_t('Public details shown to patients or staff.', 'التفاصيل العامة المعروضة للمرضى أو الموظفين.')) ?></p>
</div>
</div>
<div class="row g-3">
<div class="col-lg-4">
<label class="form-label fw-bold" 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 fw-bold" 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 class="col-lg-4">
<label class="form-label fw-bold" 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 fw-bold" 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 fw-bold" 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 class="col-lg-6">
<label class="form-label fw-bold" 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 fw-bold" 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>
<hr class="m-0 text-muted">
<!-- Branding -->
<div>
<div class="admin-section-head mb-4">
<div>
<h2 class="section-title mb-1"><?= qh_h(qh_t('Branding & Assets', 'الهوية والأصول')) ?></h2>
<p class="section-copy mb-0"><?= qh_h(qh_t('Upload logo/favicon and choose brand colors.', 'قم برفع الشعار/الأيقونة واختر ألوان الهوية.')) ?></p>
</div>
</div>
<div class="row g-4">
<div class="col-lg-6">
<label class="form-label fw-bold" for="hospitalLogoUpload"><?= qh_h(qh_t('Upload Logo', 'رفع الشعار')) ?></label>
<input id="hospitalLogoUpload" class="form-control" type="file" name="logo_upload" accept="image/*">
<?php if ($logoUrl !== ''): ?>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="remove_logo" value="1" id="removeLogo">
<label class="form-check-label text-danger" for="removeLogo">
<?= qh_h(qh_t('Remove current logo', 'إزالة الشعار الحالي')) ?>
</label>
</div>
<?php endif; ?>
</div>
<div class="col-lg-6">
<label class="form-label fw-bold" for="hospitalFaviconUpload"><?= qh_h(qh_t('Upload Favicon', 'رفع الأيقونة')) ?></label>
<input id="hospitalFaviconUpload" class="form-control" type="file" name="favicon_upload" accept="image/*">
<?php if ($faviconUrl !== ''): ?>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="remove_favicon" value="1" id="removeFavicon">
<label class="form-check-label text-danger" for="removeFavicon">
<?= qh_h(qh_t('Remove current favicon', 'إزالة الأيقونة الحالية')) ?>
</label>
</div>
<?php endif; ?>
</div>
<div class="col-lg-3">
<label class="form-label fw-bold" 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 fw-bold" 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>
<div class="d-flex flex-wrap gap-2 pt-2">
<button class="btn btn-dark btn-lg px-4" type="submit"><?= qh_h(qh_t('Save hospital profile', 'حفظ ملف المستشفى')) ?></button>
<a class="btn btn-outline-secondary btn-lg px-4" href="<?= qh_h(qh_url('admin.php')) ?>"><?= qh_h(qh_t('Cancel', 'إلغاء')) ?></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('Upload a logo 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 missing, 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(); ?>

288
admin_users.php Normal file
View File

@ -0,0 +1,288 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/queue_bootstrap.php';
qh_boot();
qh_admin_handle_request(); // To handle language swaps etc. if needed
$pdo = db();
$error = '';
$success = '';
// Handle actions
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'create_user') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$role = $_POST['role'] ?? 'admin';
if ($username === '' || $password === '') {
$error = qh_t('Username and password are required.', 'اسم المستخدم وكلمة المرور مطلوبان.');
} else {
try {
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("INSERT INTO users (username, password, role) VALUES (:username, :password, :role)");
$stmt->execute(['username' => $username, 'password' => $hash, 'role' => $role]);
$success = qh_t('User created successfully.', 'تم إنشاء المستخدم بنجاح.');
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
$error = qh_t('Username already exists.', 'اسم المستخدم موجود مسبقاً.');
} else {
$error = qh_t('Failed to create user.', 'فشل في إنشاء المستخدم.');
}
}
}
} elseif ($action === 'update_user') {
$id = (int)($_POST['id'] ?? 0);
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$role = $_POST['role'] ?? 'admin';
if ($id <= 0 || $username === '') {
$error = qh_t('Invalid user data.', 'بيانات المستخدم غير صالحة.');
} else {
try {
if ($password !== '') {
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("UPDATE users SET username = :username, password = :password, role = :role WHERE id = :id");
$stmt->execute(['username' => $username, 'password' => $hash, 'role' => $role, 'id' => $id]);
} else {
$stmt = $pdo->prepare("UPDATE users SET username = :username, role = :role WHERE id = :id");
$stmt->execute(['username' => $username, 'role' => $role, 'id' => $id]);
}
$success = qh_t('User updated successfully.', 'تم تحديث المستخدم بنجاح.');
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
$error = qh_t('Username already exists.', 'اسم المستخدم موجود مسبقاً.');
} else {
$error = qh_t('Failed to update user.', 'فشل في تحديث المستخدم.');
}
}
}
} elseif ($action === 'delete_user') {
$id = (int)($_POST['id'] ?? 0);
// Prevent deleting the last user
$count = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
if ($count <= 1) {
$error = qh_t('Cannot delete the last user in the system.', 'لا يمكن حذف آخر مستخدم في النظام.');
} elseif ($id > 0) {
$stmt = $pdo->prepare("DELETE FROM users WHERE id = :id");
$stmt->execute(['id' => $id]);
$success = qh_t('User deleted successfully.', 'تم حذف المستخدم بنجاح.');
}
}
}
try {
$users = $pdo->query("SELECT id, username, role, created_at FROM users ORDER BY id ASC")->fetchAll();
} catch (PDOException $e) {
if (str_contains($e->getMessage(), "Unknown column 'role'")) {
$pdo->exec("ALTER TABLE users ADD COLUMN role VARCHAR(20) DEFAULT 'admin'");
$users = $pdo->query("SELECT id, username, role, created_at FROM users ORDER BY id ASC")->fetchAll();
} elseif ($e->getCode() == '42S02') {
$pdo->exec("CREATE TABLE IF NOT EXISTS users (id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, role VARCHAR(20) DEFAULT 'admin', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
$users = [];
$error = qh_t('Users table was missing and has been created.', 'كان جدول المستخدمين مفقوداً وتم إنشاؤه.');
} else {
throw $e;
}
}
$stats = qh_admin_stats();
$roles = [
'admin' => qh_t('Admin', 'مدير'),
'reception' => qh_t('Reception', 'استقبال'),
'nursing' => qh_t('Nursing', 'تمريض'),
'doctor' => qh_t('Doctor', 'طبيب'),
];
qh_page_start(
'admin',
qh_t('System Users', 'مستخدمو النظام'),
qh_t('Manage system users and access.', 'إدارة مستخدمي النظام وصلاحيات الوصول.')
);
?>
<div class="container-fluid container-xxl px-3 px-lg-4">
<div class="admin-layout">
<aside class="admin-sidebar-column">
<?php qh_render_admin_sidebar('admin_users.php', $stats); ?>
</aside>
<div class="admin-content-stack">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0 text-gray-800 fw-bold"><?= qh_h(qh_t('System Users', 'مستخدمو النظام')) ?></h1>
<p class="text-muted mb-0 mt-1"><?= qh_h(qh_t('Manage system administrators and staff access.', 'إدارة مديري النظام وصلاحيات وصول الموظفين.')) ?></p>
</div>
<button type="button" class="btn btn-primary d-flex align-items-center gap-2" data-bs-toggle="modal" data-bs-target="#createUserModal">
<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
<?= qh_h(qh_t('Add User', 'إضافة مستخدم')) ?>
</button>
</div>
<?php if ($error !== ''): ?>
<div class="alert alert-danger" role="alert"><?= qh_h($error) ?></div>
<?php endif; ?>
<?php if ($success !== ''): ?>
<div class="alert alert-success" role="alert"><?= qh_h($success) ?></div>
<?php endif; ?>
<div class="card shadow-sm border-0 mb-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th scope="col" class="px-4 py-3">ID</th>
<th scope="col" class="px-4 py-3"><?= qh_h(qh_t('Username', 'اسم المستخدم')) ?></th>
<th scope="col" class="px-4 py-3"><?= qh_h(qh_t('Role / Permissions', 'الدور / الصلاحيات')) ?></th>
<th scope="col" class="px-4 py-3"><?= qh_h(qh_t('Created At', 'تاريخ الإنشاء')) ?></th>
<th scope="col" class="px-4 py-3 text-end"><?= qh_h(qh_t('Actions', 'الإجراءات')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td class="px-4 py-3 text-muted">#<?= qh_h((string)$user['id']) ?></td>
<td class="px-4 py-3 fw-medium text-gray-900"><?= qh_h($user['username']) ?></td>
<td class="px-4 py-3">
<span class="badge bg-secondary"><?= qh_h($roles[$user['role']] ?? $user['role']) ?></span>
</td>
<td class="px-4 py-3 text-muted"><?= qh_h($user['created_at']) ?></td>
<td class="px-4 py-3 text-end">
<button type="button" class="btn btn-sm btn-outline-secondary me-2"
data-bs-toggle="modal"
data-bs-target="#editUserModal"
data-id="<?= qh_h((string)$user['id']) ?>"
data-username="<?= qh_h($user['username']) ?>"
data-role="<?= qh_h($user['role'] ?? 'admin') ?>">
<?= qh_h(qh_t('Edit', 'تعديل')) ?>
</button>
<?php if (count($users) > 1): ?>
<form action="" method="POST" class="d-inline-block" onsubmit="return confirm('<?= qh_h(qh_t('Are you sure you want to delete this user?', 'هل أنت متأكد أنك تريد حذف هذا المستخدم؟')) ?>');">
<input type="hidden" name="action" value="delete_user">
<input type="hidden" name="id" value="<?= qh_h((string)$user['id']) ?>">
<button type="submit" class="btn btn-sm btn-outline-danger">
<?= qh_h(qh_t('Delete', 'حذف')) ?>
</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($users)): ?>
<tr>
<td colspan="5" class="px-4 py-4 text-center text-muted">
<?= qh_h(qh_t('No users found.', 'لا يوجد مستخدمين.')) ?>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Create User Modal -->
<div class="modal fade" id="createUserModal" tabindex="-1" aria-labelledby="createUserModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-0 shadow">
<form action="" method="POST">
<input type="hidden" name="action" value="create_user">
<div class="modal-header border-bottom-0 pb-0">
<h5 class="modal-title fw-bold" id="createUserModalLabel"><?= qh_h(qh_t('Add New User', 'إضافة مستخدم جديد')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label text-muted small fw-bold text-uppercase"><?= qh_h(qh_t('Username', 'اسم المستخدم')) ?></label>
<input type="text" name="username" class="form-control form-control-lg bg-light" required>
</div>
<div class="mb-3">
<label class="form-label text-muted small fw-bold text-uppercase"><?= qh_h(qh_t('Role / Permissions', 'الدور / الصلاحيات')) ?></label>
<select name="role" class="form-select form-select-lg bg-light" required>
<?php foreach ($roles as $key => $label): ?>
<option value="<?= qh_h($key) ?>"><?= qh_h($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label text-muted small fw-bold text-uppercase"><?= qh_h(qh_t('Password', 'كلمة المرور')) ?></label>
<input type="password" name="password" class="form-control form-control-lg bg-light" required>
</div>
</div>
<div class="modal-footer border-top-0 pt-0">
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><?= qh_h(qh_t('Cancel', 'إلغاء')) ?></button>
<button type="submit" class="btn btn-primary px-4"><?= qh_h(qh_t('Create User', 'إنشاء المستخدم')) ?></button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit User Modal -->
<div class="modal fade" id="editUserModal" tabindex="-1" aria-labelledby="editUserModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-0 shadow">
<form action="" method="POST">
<input type="hidden" name="action" value="update_user">
<input type="hidden" name="id" id="editUserId" value="">
<div class="modal-header border-bottom-0 pb-0">
<h5 class="modal-title fw-bold" id="editUserModalLabel"><?= qh_h(qh_t('Edit User', 'تعديل المستخدم')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label text-muted small fw-bold text-uppercase"><?= qh_h(qh_t('Username', 'اسم المستخدم')) ?></label>
<input type="text" name="username" id="editUserUsername" class="form-control form-control-lg bg-light" required>
</div>
<div class="mb-3">
<label class="form-label text-muted small fw-bold text-uppercase"><?= qh_h(qh_t('Role / Permissions', 'الدور / الصلاحيات')) ?></label>
<select name="role" id="editUserRole" class="form-select form-select-lg bg-light" required>
<?php foreach ($roles as $key => $label): ?>
<option value="<?= qh_h($key) ?>"><?= qh_h($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label text-muted small fw-bold text-uppercase"><?= qh_h(qh_t('New Password (Optional)', 'كلمة مرور جديدة (اختياري)')) ?></label>
<input type="password" name="password" class="form-control form-control-lg bg-light" placeholder="<?= qh_h(qh_t('Leave blank to keep current password', 'اتركه فارغاً للاحتفاظ بكلمة المرور الحالية')) ?>">
</div>
</div>
<div class="modal-footer border-top-0 pt-0">
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><?= qh_h(qh_t('Cancel', 'إلغاء')) ?></button>
<button type="submit" class="btn btn-primary px-4"><?= qh_h(qh_t('Save Changes', 'حفظ التغييرات')) ?></button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const editModal = document.getElementById('editUserModal');
if (editModal) {
editModal.addEventListener('show.bs.modal', function (event) {
const button = event.relatedTarget;
const id = button.getAttribute('data-id');
const username = button.getAttribute('data-username');
const role = button.getAttribute('data-role');
editModal.querySelector('#editUserId').value = id;
editModal.querySelector('#editUserUsername').value = username;
const roleSelect = editModal.querySelector('#editUserRole');
if (roleSelect && role) {
roleSelect.value = role;
}
});
}
});
</script>
<?php
qh_page_end();

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -1,39 +1,305 @@
document.addEventListener('DOMContentLoaded', () => {
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const chatMessages = document.getElementById('chat-messages');
const clinicSelects = document.querySelectorAll('.js-clinic-select');
clinicSelects.forEach((clinicSelect) => {
const form = clinicSelect.closest('form');
if (!form) return;
const doctorSelect = form.querySelector('.js-doctor-select');
if (!doctorSelect) return;
const appendMessage = (text, sender) => {
const msgDiv = document.createElement('div');
msgDiv.classList.add('message', sender);
msgDiv.textContent = text;
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
};
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const message = chatInput.value.trim();
if (!message) return;
appendMessage(message, 'visitor');
chatInput.value = '';
try {
const response = await fetch('api/chat.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
const syncDoctors = () => {
const clinicId = clinicSelect.value;
Array.from(doctorSelect.options).forEach((option) => {
if (!option.value) {
option.hidden = false;
return;
}
const visible = option.dataset.clinicId === clinicId;
option.hidden = !visible;
if (!visible && option.selected) {
doctorSelect.value = '';
}
});
const data = await response.json();
// Artificial delay for realism
setTimeout(() => {
appendMessage(data.reply, 'bot');
}, 500);
} catch (error) {
console.error('Error:', error);
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
};
clinicSelect.addEventListener('change', syncDoctors);
syncDoctors();
});
const ticketPrintButton = document.querySelector('.js-print-ticket');
if (ticketPrintButton) {
ticketPrintButton.addEventListener('click', () => window.print());
}
document.querySelectorAll('.js-app-toast').forEach((toastNode) => {
if (window.bootstrap && bootstrap.Toast) {
bootstrap.Toast.getOrCreateInstance(toastNode, { delay: 3200 }).show();
}
});
});
const locale = document.body.dataset.locale || 'en';
const timeZone = document.body.dataset.timezone || 'UTC';
const liveClock = document.querySelector('.js-live-clock');
if (liveClock) {
const renderClock = () => {
const now = new Date();
const opts = { hour: '2-digit', minute: '2-digit' };
if (timeZone) opts.timeZone = timeZone;
try {
liveClock.textContent = now.toLocaleTimeString(locale === 'ar' ? 'ar-SA' : 'en-US', opts);
} catch (e) {
opts.timeZone = undefined;
liveClock.textContent = now.toLocaleTimeString(locale === 'ar' ? 'ar-SA' : 'en-US', opts);
}
};
renderClock();
window.setInterval(renderClock, 1000 * 30);
}
if (document.body.dataset.page === 'display') {
const fullscreenButton = document.querySelector('.js-fullscreen-toggle');
const syncFullscreenButton = () => {
if (!fullscreenButton) return;
const isFullscreen = !!document.fullscreenElement;
fullscreenButton.title = isFullscreen
? (fullscreenButton.dataset.labelExit || 'Exit full display')
: (fullscreenButton.dataset.labelEnter || 'Full display');
const icon = fullscreenButton.querySelector('i');
if (icon) {
icon.className = isFullscreen ? 'bi bi-fullscreen-exit' : 'bi bi-arrows-fullscreen';
}
fullscreenButton.setAttribute('aria-pressed', isFullscreen ? 'true' : 'false');
};
if (fullscreenButton && document.fullscreenEnabled) {
fullscreenButton.addEventListener('click', async () => {
try {
if (document.fullscreenElement) {
await document.exitFullscreen();
window.localStorage.setItem('hospitalQueue:autoFullscreen', 'false');
} else {
await document.documentElement.requestFullscreen();
window.localStorage.setItem('hospitalQueue:autoFullscreen', 'true');
}
} catch (error) {
console.warn('Fullscreen toggle failed', error);
}
});
document.addEventListener('fullscreenchange', syncFullscreenButton);
syncFullscreenButton();
if (window.localStorage.getItem('hospitalQueue:autoFullscreen') === 'true') {
const autoFs = async () => {
if (!document.fullscreenElement) {
try { await document.documentElement.requestFullscreen(); } catch (e) {}
}
document.removeEventListener('click', autoFs);
document.removeEventListener('keydown', autoFs);
document.removeEventListener('touchstart', autoFs);
};
try {
document.documentElement.requestFullscreen().catch(() => {
document.addEventListener('click', autoFs);
document.addEventListener('keydown', autoFs);
document.addEventListener('touchstart', autoFs);
});
} catch (e) {
document.addEventListener('click', autoFs);
document.addEventListener('keydown', autoFs);
document.addEventListener('touchstart', autoFs);
}
}
} else if (fullscreenButton) {
fullscreenButton.hidden = true;
}
let audioCtx = null;
const initAudio = () => {
try {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!audioCtx && AudioContext) {
audioCtx = new AudioContext();
}
if (audioCtx && audioCtx.state === 'suspended') {
audioCtx.resume();
}
} catch (e) {
console.warn('AudioContext init failed', e);
}
};
let availableVoices = [];
if ('speechSynthesis' in window) {
const updateVoices = () => { availableVoices = window.speechSynthesis.getVoices(); };
updateVoices();
if (window.speechSynthesis.onvoiceschanged !== undefined) {
window.speechSynthesis.onvoiceschanged = updateVoices;
}
}
const audioBtn = document.querySelector('.js-audio-toggle');
if (audioBtn) {
const updateAudioBtnState = () => {
const isEnabled = window.localStorage.getItem('hospitalQueue:audioEnabled') === 'true';
const icon = audioBtn.querySelector('i');
if (icon) {
icon.className = isEnabled ? 'bi bi-megaphone-fill' : 'bi bi-megaphone';
}
audioBtn.setAttribute('aria-pressed', isEnabled.toString());
const videoPlayer = document.getElementById('adsVideoPlayer');
if (videoPlayer) {
videoPlayer.muted = !isEnabled;
}
};
updateAudioBtnState();
audioBtn.addEventListener('click', () => {
const isEnabled = window.localStorage.getItem('hospitalQueue:audioEnabled') === 'true';
const nextState = !isEnabled;
window.localStorage.setItem('hospitalQueue:audioEnabled', nextState.toString());
updateAudioBtnState();
if (nextState) {
initAudio();
if ('speechSynthesis' in window) {
const primeUtterance = new SpeechSynthesisUtterance('');
window.speechSynthesis.speak(primeUtterance);
}
}
});
}
const playChime = () => {
try {
if (!audioCtx) initAudio();
if (!audioCtx) return;
if (audioCtx.state === 'suspended') audioCtx.resume();
const playTone = (freq, startTime, duration) => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, audioCtx.currentTime + startTime);
gain.gain.setValueAtTime(0, audioCtx.currentTime + startTime);
gain.gain.linearRampToValueAtTime(0.5, audioCtx.currentTime + startTime + 0.05);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + startTime + duration);
osc.start(audioCtx.currentTime + startTime);
osc.stop(audioCtx.currentTime + startTime + duration);
};
playTone(523.25, 0, 0.4); // C5
playTone(659.25, 0.3, 0.6); // E5
} catch (e) {
console.warn('AudioContext failed', e);
}
};
const checkAnnouncements = () => {
const cards = Array.from(document.querySelectorAll('.announcement-card'));
const latest = cards[0];
const audioEnabled = window.localStorage.getItem('hospitalQueue:audioEnabled') === 'true';
if (latest) {
const announcementKey = latest.dataset.announcementKey || '';
const storageKey = `hospitalQueue:lastAnnouncement:${locale}`;
const storedKey = window.localStorage.getItem(storageKey) || '';
if (announcementKey && announcementKey !== storedKey) {
window.localStorage.setItem(storageKey, announcementKey);
if (audioEnabled) {
const videoPlayer = document.getElementById('adsVideoPlayer');
if (videoPlayer) videoPlayer.volume = 0.1;
playChime();
setTimeout(() => {
const text = locale === 'ar' ? (latest.dataset.announcementAr || '') : (latest.dataset.announcementEn || '');
if (text) {
// Use Google Translate TTS (outside high-quality voice API)
const tl = locale === 'ar' ? 'ar' : 'en';
const gTtsUrl = 'https://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&tl=' + tl + '&q=' + encodeURIComponent(text);
const audio = new Audio(gTtsUrl);
audio.onended = () => { if (videoPlayer) videoPlayer.volume = 1.0; };
let fallbackPlayed = false;
const handleFallback = (err) => {
if (fallbackPlayed) return;
fallbackPlayed = true;
// Fallback to built-in if external TTS fails
console.warn("External TTS failed, falling back to built-in speech", err);
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = locale === 'ar' ? 'ar-SA' : 'en-US';
const voices = availableVoices.length > 0 ? availableVoices : window.speechSynthesis.getVoices();
const langPrefix = locale === 'ar' ? 'ar' : 'en';
const langVoices = voices.filter(v => v.lang.toLowerCase().startsWith(langPrefix));
if (langVoices.length > 0) {
const bestVoice = langVoices.find(v =>
v.name.includes('Google') || v.name.includes('Natural') || v.name.includes('Premium') || v.name.includes('Online')
) || langVoices.find(v => v.name.includes('Microsoft')) || langVoices[0];
if (bestVoice) utterance.voice = bestVoice;
}
utterance.onend = () => { if (videoPlayer) videoPlayer.volume = 1.0; };
utterance.onerror = () => { if (videoPlayer) videoPlayer.volume = 1.0; };
window.speechSynthesis.speak(utterance);
} else {
if (videoPlayer) videoPlayer.volume = 1.0;
}
};
audio.onerror = handleFallback;
// Attempt to play external audio
const playPromise = audio.play();
if (playPromise !== undefined) {
playPromise.catch(handleFallback);
}
} else {
if (videoPlayer) videoPlayer.volume = 1.0;
}
}, 1200); // play voice after chime finishes
}
}
}
};
checkAnnouncements();
const autoRefreshSeconds = parseInt(document.querySelector('[data-auto-refresh]')?.dataset.autoRefresh || '0', 10);
if (autoRefreshSeconds > 0) {
window.setInterval(async () => {
try {
const response = await fetch(window.location.href, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
if (response.ok) {
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newSections = doc.querySelector('#queueSections');
const currentSections = document.querySelector('#queueSections');
if (newSections && currentSections) {
currentSections.innerHTML = newSections.innerHTML;
}
const oldTicker = document.querySelector('.news-ticker-container');
const newTicker = doc.querySelector('.news-ticker-container');
if (oldTicker && newTicker) {
oldTicker.innerHTML = newTicker.innerHTML;
}
checkAnnouncements();
}
} catch (e) {
console.error('Auto-refresh failed', e);
}
}, autoRefreshSeconds * 1000);
}
}
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

243
display.php Normal file
View File

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

125
doctor.php Normal file
View File

@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/queue_bootstrap.php';
qh_boot();
qh_doctor_handle_request();
$doctors = qh_fetch_doctors();
$selectedDoctorId = isset($_GET['doctor_id']) ? (int) $_GET['doctor_id'] : (int) ($doctors[0]['id'] ?? 0);
$selectedDoctor = $selectedDoctorId > 0 ? qh_fetch_doctor($selectedDoctorId) : null;
$doctorQueue = $selectedDoctorId > 0 ? qh_fetch_tickets(['ready_for_doctor', 'called', 'in_progress'], $selectedDoctorId, 20) : [];
qh_page_start(
'doctor',
qh_t('Doctor queue', 'طابور الطبيب'),
qh_t('Doctor workspace.', 'مساحة عمل الطبيب.')
);
?>
<div class="container-fluid container-xxl px-3 px-lg-4">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-4">
<div>
<h1 class="h3 mb-0 text-gray-800 fw-bold"><?= qh_h(qh_t('Doctor Workspace', 'مساحة عمل الطبيب')) ?></h1>
<p class="text-muted mb-0 mt-1"><?= qh_h(qh_t('Call the next patient and manage room flow.', 'نادِ المريض التالي وأدر حركة الغرفة.')) ?></p>
</div>
</div>
<div class="row g-4">
<div class="col-xl-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white border-bottom py-3">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Select Room', 'اختيار الغرفة')) ?></h5>
</div>
<div class="card-body">
<form method="get" class="vstack gap-3">
<input type="hidden" name="lang" value="<?= qh_h(qh_locale()) ?>">
<div>
<label class="form-label fw-semibold text-dark"><?= qh_h(qh_t('Doctor room', 'غرفة الطبيب')) ?></label>
<select class="form-select bg-light" name="doctor_id" onchange="this.form.submit()">
<?php foreach ($doctors as $doctor): ?>
<option value="<?= qh_h((string) $doctor['id']) ?>" <?= (int) $doctor['id'] === $selectedDoctorId ? 'selected' : '' ?>><?= qh_h(qh_name($doctor)) ?> · <?= qh_h(qh_t('Rm', 'غرفة')) ?> <?= qh_h($doctor['room_number']) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php if ($selectedDoctor): ?>
<div class="p-3 bg-light rounded border mt-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1 small"><?= qh_h(qh_t('Active Doctor', 'الطبيب النشط')) ?></div>
<div class="fw-bold text-dark fs-5 mb-1"><?= qh_h(qh_name($selectedDoctor)) ?></div>
<div class="text-muted small"><i class="bi bi-door-open me-1"></i><?= qh_h(qh_t('Room', 'الغرفة')) ?> <?= qh_h($selectedDoctor['room_number'] ?? '--') ?></div>
</div>
<?php endif; ?>
<a class="btn btn-outline-secondary bg-white shadow-sm mt-3 w-100" href="<?= qh_h(qh_url('display.php')) ?>" target="_blank" rel="noopener"><i class="bi bi-display me-2"></i><?= qh_h(qh_t('Open Display', 'فتح الشاشة العامة')) ?></a>
</form>
</div>
</div>
</div>
<div class="col-xl-8">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Room Queue', 'قائمة الإنتظار')) ?></h5>
</div>
<div class="card-body p-0">
<?php if (!$selectedDoctor): ?>
<div class="text-center py-5 text-muted">
<p class="mb-0"><?= qh_h(qh_t('Please select a doctor to view the queue.', 'يرجى اختيار طبيب لعرض الطابور.')) ?></p>
</div>
<?php elseif (empty($doctorQueue)): ?>
<div class="text-center py-5 text-muted">
<p class="mb-0"><?= qh_h(qh_t('No patients waiting for this room.', 'لا يوجد مرضى في انتظار هذه الغرفة.')) ?></p>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="border-0 px-4 py-3"><?= qh_h(qh_t('Ticket', 'التذكرة')) ?></th>
<th class="border-0 py-3"><?= qh_h(qh_t('Patient', 'المريض')) ?></th>
<th class="border-0 py-3"><?= qh_h(qh_t('Status', 'الحالة')) ?></th>
<th class="border-0 px-4 py-3 text-end"><?= qh_h(qh_t('Action', 'الإجراء')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($doctorQueue as $ticket): ?>
<tr>
<td class="fw-bold px-4 py-3 text-dark"><?= qh_h($ticket['ticket_number']) ?></td>
<td class="py-3 fw-semibold text-dark"><?= qh_h($ticket['patient_name']) ?></td>
<td class="py-3"><?= qh_status_badge($ticket['status']) ?></td>
<td class="text-end px-4 py-3">
<form method="post" class="d-inline-flex gap-2 flex-wrap justify-content-end align-items-center">
<input type="hidden" name="ticket_id" value="<?= qh_h((string) $ticket['id']) ?>"> <input type="hidden" name="doctor_id" value="<?= qh_h((string) $selectedDoctorId) ?>">
<?php if ($ticket['status'] === 'ready_for_doctor'): ?>
<button class="btn btn-sm btn-primary shadow-sm" type="submit" name="action" value="call_ticket"><?= qh_h(qh_t('Call', 'نداء')) ?></button>
<?php else: ?>
<button class="btn btn-sm btn-warning shadow-sm" type="submit" name="action" value="call_ticket"><?= qh_h(qh_t('Recall', 'إعادة النداء')) ?></button>
<?php endif; ?>
<button class="btn btn-sm btn-outline-danger shadow-sm bg-white" type="submit" name="action" value="mark_no_show"><?= qh_h(qh_t('Not Show', 'غائب')) ?></button>
<button class="btn btn-sm btn-success shadow-sm" type="submit" name="action" value="complete_ticket"><?= qh_h(qh_t('Served', 'تمت الخدمة')) ?></button>
<div class="input-group input-group-sm ms-lg-2 mt-2 mt-lg-0" style="max-width: 180px;">
<select name="refer_to_doctor_id" class="form-select border-secondary text-secondary">
<option value=""><?= qh_h(qh_t('Refer to...', 'تحويل إلى...')) ?></option>
<?php foreach ($doctors as $d): if ($d['id'] == $selectedDoctorId) continue; ?>
<option value="<?= qh_h((string) $d['id']) ?>"><?= qh_h(qh_name($d)) ?></option>
<?php endforeach; ?>
</select>
<button class="btn btn-outline-secondary bg-white" type="submit" name="action" value="refer_ticket" title="<?= qh_h(qh_t('Refer Patient', 'تحويل المريض')) ?>">
<i class="bi bi-arrow-right-circle"></i>
</button>
</div>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<?php qh_page_end(); ?>

306
index.php
View File

@ -1,150 +1,166 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
require_once __DIR__ . '/queue_bootstrap.php';
qh_boot();
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
$stats = qh_dashboard_stats();
$overview = qh_queue_overview();
$recentTickets = qh_fetch_tickets(['waiting_vitals', 'ready_for_doctor', 'called', 'in_progress'], null, 8);
$calledTickets = qh_fetch_tickets(['called', 'in_progress'], null, 4);
qh_page_start(
'home',
qh_t('Hospital queue operations', 'عمليات طابور المستشفى'),
qh_t('English operations dashboard for the hospital queue workflow.', 'لوحة عمليات عربية لمسار طابور المستشفى.')
);
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
<div class="container-fluid container-xxl px-3 px-lg-4">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-4">
<div>
<h1 class="h3 mb-0 text-gray-800 fw-bold"><?= qh_h(qh_t('Operations Dashboard', 'لوحة عمليات المستشفى')) ?></h1>
<p class="text-muted mb-0 mt-1"><?= qh_h(qh_t('Real-time hospital queue system metrics.', 'إحصائيات لحظية لنظام طوابير المستشفى.')) ?></p>
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
</body>
</html>
<div class="d-flex gap-2">
<a class="btn btn-primary shadow-sm" href="<?= qh_h(qh_url('reception.php')) ?>"><?= qh_h(qh_t('Issue ticket', 'إصدار تذكرة')) ?></a>
<a class="btn btn-outline-secondary shadow-sm bg-white" href="<?= qh_h(qh_url('display.php')) ?>" target="_blank"><?= qh_h(qh_t('Public display', 'الشاشة العامة')) ?></a>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-md-3 col-sm-6">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1 small"><?= qh_h(qh_t('Issued today', 'التذاكر الصادرة اليوم')) ?></div>
<div class="h3 mb-0 font-weight-bold text-gray-800"><?= qh_h((string) $stats['issued_today']) ?></div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1 small"><?= qh_h(qh_t('Waiting vitals', 'بانتظار العلامات')) ?></div>
<div class="h3 mb-0 font-weight-bold text-gray-800"><?= qh_h((string) $stats['waiting_vitals']) ?></div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1 small"><?= qh_h(qh_t('Ready for doctor', 'جاهز للطبيب')) ?></div>
<div class="h3 mb-0 font-weight-bold text-gray-800"><?= qh_h((string) $stats['ready_for_doctor']) ?></div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1 small"><?= qh_h(qh_t('Active rooms', 'الغرف النشطة')) ?></div>
<div class="h3 mb-0 font-weight-bold text-gray-800"><?= qh_h((string) $stats['active_rooms']) ?></div>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-xl-8">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Live queue overview', 'نظرة مباشرة على الطابور')) ?></h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="border-0 px-4 py-3"><?= qh_h(qh_t('Clinic', 'العيادة')) ?></th>
<th class="border-0 py-3"><?= qh_h(qh_t('Vitals wait', 'انتظار العلامات')) ?></th>
<th class="border-0 py-3"><?= qh_h(qh_t('Doctor wait', 'انتظار الطبيب')) ?></th>
<th class="border-0 py-3"><?= qh_h(qh_t('Active calls', 'النداءات النشطة')) ?></th>
<th class="border-0 text-end px-4 py-3"><?= qh_h(qh_t('Today total', 'إجمالي اليوم')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($overview as $row): ?>
<tr>
<td class="fw-semibold px-4 py-3 text-dark"><?= qh_h(qh_name($row)) ?></td>
<td class="py-3"><span class="badge bg-warning text-dark px-2 py-1 rounded-pill"><?= qh_h((string) $row['vitals_waiting']) ?></span></td>
<td class="py-3"><span class="badge bg-info text-dark px-2 py-1 rounded-pill"><?= qh_h((string) $row['doctor_waiting']) ?></span></td>
<td class="py-3"><span class="badge bg-dark px-2 py-1 rounded-pill"><?= qh_h((string) $row['active_calls']) ?></span></td>
<td class="text-end px-4 py-3 text-muted"><?= qh_h((string) $row['total_today']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-xl-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white border-bottom py-3">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Current calls', 'النداءات الحالية')) ?></h5>
</div>
<div class="card-body">
<?php if ($calledTickets): ?>
<div class="vstack gap-3">
<?php foreach ($calledTickets as $ticket): ?>
<div class="d-flex justify-content-between align-items-center p-3 border rounded bg-light">
<div>
<div class="fw-bold h5 mb-1 text-dark"><?= qh_h($ticket['ticket_number']) ?></div>
<div class="small text-muted"><?= qh_h(qh_name($ticket, 'doctor_name', qh_t('Unassigned', 'غير محدد'))) ?> &bull; <?= qh_h(qh_t('Room', 'غرفة')) ?> <?= qh_h($ticket['doctor_room'] ?? '--') ?></div>
</div>
<?= qh_status_badge($ticket['status']) ?>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="text-center py-4 text-muted">
<p class="mb-0"><?= qh_h(qh_t('No active calls yet.', 'لا توجد نداءات نشطة حالياً.')) ?></p>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom py-3">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Recent patient flow', 'آخر حركة للمرضى')) ?></h5>
</div>
<div class="card-body p-0">
<?php if ($recentTickets): ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="border-0 px-4 py-3"><?= qh_h(qh_t('Ticket', 'التذكرة')) ?></th>
<th class="border-0 py-3"><?= qh_h(qh_t('Patient', 'المريض')) ?></th>
<th class="border-0 py-3"><?= qh_h(qh_t('Clinic', 'العيادة')) ?></th>
<th class="border-0 py-3"><?= qh_h(qh_t('Doctor', 'الطبيب')) ?></th>
<th class="border-0 py-3"><?= qh_h(qh_t('Status', 'الحالة')) ?></th>
<th class="border-0 px-4 py-3 text-end"></th>
</tr>
</thead>
<tbody>
<?php foreach ($recentTickets as $ticket): ?>
<tr>
<td class="fw-bold px-4 py-3 text-dark"><?= qh_h($ticket['ticket_number']) ?></td>
<td class="py-3"><?= qh_h($ticket['patient_name']) ?></td>
<td class="py-3 text-muted"><?= qh_h(qh_name($ticket, 'clinic_name')) ?></td>
<td class="py-3 text-muted"><?= qh_h(qh_name($ticket, 'doctor_name', qh_t('Unassigned', 'غير محدد'))) ?></td>
<td class="py-3"><?= qh_status_badge($ticket['status']) ?></td>
<td class="text-end px-4 py-3"><a class="btn btn-sm btn-light border" href="<?= qh_h(qh_url('ticket.php', ['id' => (int) $ticket['id']])) ?>"><?= qh_h(qh_t('View', 'عرض')) ?></a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="text-center py-5 text-muted">
<p class="mb-0"><?= qh_h(qh_t('No recent tickets yet.', 'لا توجد تذاكر حديثة حتى الآن.')) ?></p>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php qh_page_end(); ?>

1
install.lock Normal file
View File

@ -0,0 +1 @@
2026-04-01 09:15:21

213
install.php Normal file
View File

@ -0,0 +1,213 @@
<?php
declare(strict_types=1);
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
if (file_exists(__DIR__ . '/.installed') || file_exists(__DIR__ . '/install.lock')) {
die('Installation already completed. To reinstall, delete the .installed file.');
}
$step = isset($_GET['step']) ? (int)$_GET['step'] : 1;
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$step = isset($_POST['step']) ? (int)$_POST['step'] : $step;
if ($step === 3) {
$dbHost = $_POST['db_host'] ?? '';
$dbName = $_POST['db_name'] ?? '';
$dbUser = $_POST['db_user'] ?? '';
$dbPass = $_POST['db_pass'] ?? '';
try {
$pdo = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8mb4", $dbUser, $dbPass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
// Rewrite db/config.php
$configContent = "<?php\n" .
"define('DB_HOST', '$dbHost');\n" .
"define('DB_NAME', '$dbName');\n" .
"define('DB_USER', '$dbUser');\n" .
"define('DB_PASS', '$dbPass');\n\n" .
"function db() {\n" .
" static \$pdo;\n" .
" if (!\$pdo) {\n" .
" \$pdo = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4', DB_USER, DB_PASS, [\n" .
" PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,\n" .
" PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,\n" .
" ]);\n" .
" }\n" .
" return \$pdo;\n" .
"}\n";
file_put_contents(__DIR__ . '/db/config.php', $configContent);
header('Location: install.php?step=4');
exit;
} catch (Exception $e) {
$error = 'Database Connection Failed: ' . $e->getMessage();
}
} elseif ($step === 4) {
require_once __DIR__ . '/db/config.php';
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
if (empty($username) || empty($password)) {
$error = 'Please provide both username and password.';
} else {
try {
$pdo = db();
$pdo->exec("CREATE TABLE IF NOT EXISTS users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("INSERT INTO users (username, password) VALUES (:username, :password) ON DUPLICATE KEY UPDATE password = :password");
$stmt->execute(['username' => $username, 'password' => $hash]);
header('Location: install.php?step=5');
exit;
} catch (Exception $e) {
$error = 'Failed to create user: ' . $e->getMessage();
}
}
} elseif ($step === 5) {
file_put_contents(__DIR__ . '/.installed', date('Y-m-d H:i:s'));
file_put_contents(__DIR__ . '/install.lock', date('Y-m-d H:i:s'));
header('Location: login.php');
exit;
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Installation</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background-color: #f8f9fa; }
.install-card { border: none; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
</style>
</head>
<body>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<div class="card install-card">
<div class="card-header bg-white border-bottom-0 pt-4 pb-0">
<h3 class="card-title text-center text-primary mb-0 fw-bold">System Installation</h3>
<div class="text-center text-muted mt-2">Step <?= $step ?> of 5</div>
</div>
<div class="card-body p-4">
<?php if ($error): ?>
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<?php if ($step === 1): ?>
<h4 class="mb-3 text-dark">Welcome</h4>
<p class="text-secondary">Welcome to the Hospital Queue Center installation wizard. This will set up your environment, configure the database variables, and create your super admin account so you are ready to go.</p>
<div class="text-end mt-4">
<a href="install.php?step=2" class="btn btn-primary px-4 py-2 rounded-pill">Next: Environment Check</a>
</div>
<?php elseif ($step === 2): ?>
<h4 class="mb-3 text-dark">Environment Check</h4>
<ul class="list-group mb-4 rounded-3">
<?php
$phpOk = version_compare(PHP_VERSION, '8.0.0', '>=');
$pdoOk = extension_loaded('pdo_mysql');
$dbWritable = is_writable(__DIR__ . '/db') || is_writable(__DIR__ . '/db/config.php');
?>
<li class="list-group-item d-flex justify-content-between align-items-center">
PHP Version (>= 8.0)
<span class="badge bg-<?= $phpOk ? 'success' : 'danger' ?> rounded-pill px-3 py-2"><?= PHP_VERSION ?></span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
PDO MySQL Extension
<span class="badge bg-<?= $pdoOk ? 'success' : 'danger' ?> rounded-pill px-3 py-2"><?= $pdoOk ? 'Yes' : 'No' ?></span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
DB Config Writable
<span class="badge bg-<?= $dbWritable ? 'success' : 'danger' ?> rounded-pill px-3 py-2"><?= $dbWritable ? 'Yes' : 'No' ?></span>
</li>
</ul>
<?php if ($phpOk && $pdoOk && $dbWritable): ?>
<div class="text-end">
<a href="install.php?step=3" class="btn btn-primary px-4 py-2 rounded-pill">Next: Database Variables</a>
</div>
<?php else: ?>
<div class="alert alert-warning">Please fix the environment issues before proceeding.</div>
<?php endif; ?>
<?php elseif ($step === 3): ?>
<h4 class="mb-3 text-dark">Database Variables</h4>
<?php
$dbHost = defined('DB_HOST') ? DB_HOST : '127.0.0.1';
$dbName = defined('DB_NAME') ? DB_NAME : '';
$dbUser = defined('DB_USER') ? DB_USER : '';
$dbPass = defined('DB_PASS') ? DB_PASS : '';
if (file_exists(__DIR__ . '/db/config.php')) {
$content = file_get_contents(__DIR__ . '/db/config.php');
if (preg_match("/define\('DB_HOST',\s*'([^']+)'\)/", $content, $m)) $dbHost = $m[1];
if (preg_match("/define\('DB_NAME',\s*'([^']+)'\)/", $content, $m)) $dbName = $m[1];
if (preg_match("/define\('DB_USER',\s*'([^']+)'\)/", $content, $m)) $dbUser = $m[1];
if (preg_match("/define\('DB_PASS',\s*'([^']+)'\)/", $content, $m)) $dbPass = $m[1];
}
?>
<form method="POST" action="install.php">
<input type="hidden" name="step" value="3">
<div class="mb-3">
<label class="form-label text-secondary fw-semibold">Database Host</label>
<input type="text" name="db_host" class="form-control form-control-lg bg-light" value="<?= htmlspecialchars($dbHost) ?>" required>
</div>
<div class="mb-3">
<label class="form-label text-secondary fw-semibold">Database Name</label>
<input type="text" name="db_name" class="form-control form-control-lg bg-light" value="<?= htmlspecialchars($dbName) ?>" required>
</div>
<div class="mb-3">
<label class="form-label text-secondary fw-semibold">Database User</label>
<input type="text" name="db_user" class="form-control form-control-lg bg-light" value="<?= htmlspecialchars($dbUser) ?>" required>
</div>
<div class="mb-3">
<label class="form-label text-secondary fw-semibold">Database Password</label>
<input type="password" name="db_pass" class="form-control form-control-lg bg-light" value="<?= htmlspecialchars($dbPass) ?>" required>
</div>
<div class="text-end mt-4">
<button type="submit" class="btn btn-primary px-4 py-2 rounded-pill">Save & Next</button>
</div>
</form>
<?php elseif ($step === 4): ?>
<h4 class="mb-3 text-dark">Super Admin Credentials</h4>
<form method="POST" action="install.php">
<input type="hidden" name="step" value="4">
<div class="mb-3">
<label class="form-label text-secondary fw-semibold">Admin Username</label>
<input type="text" name="username" class="form-control form-control-lg bg-light" value="admin" required>
</div>
<div class="mb-3">
<label class="form-label text-secondary fw-semibold">Admin Password</label>
<input type="password" name="password" class="form-control form-control-lg bg-light" required>
</div>
<div class="text-end mt-4">
<button type="submit" class="btn btn-primary px-4 py-2 rounded-pill">Create Admin & Next</button>
</div>
</form>
<?php elseif ($step === 5): ?>
<h4 class="mb-3 text-success fw-bold">Ready to go!</h4>
<p class="text-secondary mb-4">The installation has been successfully completed. Your database is set up and the super admin account is ready.</p>
<form method="POST" action="install.php">
<input type="hidden" name="step" value="5">
<button type="submit" class="btn btn-success w-100 py-3 rounded-pill fw-bold" style="font-size: 1.1rem;">Finish & Go to Login</button>
</form>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

100
login.php Normal file
View File

@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/queue_bootstrap.php';
if (!empty($_SESSION['user_id'])) {
qh_redirect('index.php');
}
$error = '';
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
$username = trim((string) ($_POST['username'] ?? ''));
$password = (string) ($_POST['password'] ?? '');
if ($username === '' || $password === '') {
$error = qh_t('Please enter your username and password.', 'يرجى إدخال اسم المستخدم وكلمة المرور.');
} else {
try {
$stmt = db()->prepare("SELECT id, password, role FROM users WHERE username = :username LIMIT 1");
$stmt->execute(['username' => $username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
$_SESSION['user_id'] = (int) $user['id'];
$_SESSION['username'] = $username;
$_SESSION['role'] = $user['role'] ?? 'admin';
qh_redirect('index.php');
} else {
$error = qh_t('Invalid username or password.', 'اسم المستخدم أو كلمة المرور غير صحيحة.');
}
} catch (Throwable $e) {
$error = qh_t('Login failed due to a system error.', 'فشل تسجيل الدخول بسبب خطأ في النظام.');
}
}
}
qh_page_start(
'login',
qh_t('Sign In', 'تسجيل الدخول'),
qh_t('Sign in to the hospital queue system.', 'تسجيل الدخول إلى نظام طوابير المستشفى.')
);
?>
<style>
.login-wrapper {
min-height: calc(100vh - 200px);
display: flex;
align-items: center;
justify-content: center;
}
.login-card {
width: 100%;
max-width: 420px;
border: none;
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0,0,0,0.08);
overflow: hidden;
}
.login-header {
background: var(--accent, #0F8B8D);
color: white;
padding: 2rem 1.5rem;
text-align: center;
}
.login-body {
padding: 2rem;
background: #fff;
}
</style>
<div class="container-fluid container-xxl px-3 px-lg-4">
<div class="login-wrapper">
<div class="card login-card">
<div class="login-header">
<h3 class="mb-0 fw-bold"><?= qh_h(qh_hospital_name()) ?></h3>
<p class="text-white-50 mt-2 mb-0"><?= qh_h(qh_t('Secure Access', 'الوصول الآمن')) ?></p>
</div>
<div class="login-body">
<?php if ($error !== ''): ?>
<div class="alert alert-danger mb-4"><?= qh_h($error) ?></div>
<?php endif; ?>
<form method="POST" action="<?= qh_h(qh_url('login.php')) ?>">
<div class="mb-4">
<label class="form-label text-secondary fw-semibold"><?= qh_h(qh_t('Username', 'اسم المستخدم')) ?></label>
<input type="text" name="username" class="form-control form-control-lg bg-light" required autofocus>
</div>
<div class="mb-4">
<label class="form-label text-secondary fw-semibold"><?= qh_h(qh_t('Password', 'كلمة المرور')) ?></label>
<input type="password" name="password" class="form-control form-control-lg bg-light" required>
</div>
<button type="submit" class="btn btn-primary w-100 py-3 rounded-pill fw-bold" style="font-size: 1.1rem;">
<?= qh_h(qh_t('Sign In', 'تسجيل الدخول')) ?>
</button>
</form>
</div>
</div>
</div>
</div>
<?php qh_page_end(); ?>

12
logout.php Normal file
View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
$_SESSION = [];
session_destroy();
header('Location: login.php');
exit;

66
nursing.php Normal file
View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/queue_bootstrap.php';
qh_boot();
qh_nursing_handle_request();
$waitingTickets = qh_fetch_tickets(['waiting_vitals', 'nursing_called'], null, 20);
qh_page_start(
'nursing',
qh_t('Nursing vitals', 'علامات التمريض'),
qh_t('Nursing workspace.', 'مساحة عمل التمريض.')
);
?>
<div class="container-fluid container-xxl px-3 px-lg-4">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-4">
<div>
<h1 class="h3 mb-0 text-gray-800 fw-bold"><?= qh_h(qh_t('Nursing Workspace', 'مساحة عمل التمريض')) ?></h1>
<p class="text-muted mb-0 mt-1"><?= qh_h(qh_t('Capture vitals and release patients to doctors.', 'سجّل العلامات الحيوية ثم حوّل المرضى إلى الأطباء.')) ?></p>
</div>
</div>
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Waiting for Vitals', 'بانتظار العلامات الحيوية')) ?></h5>
<span class="badge bg-secondary rounded-pill"><?= qh_h(count($waitingTickets) . ' ' . qh_t('patients', 'مرضى')) ?></span>
</div>
<div class="card-body">
<?php if ($waitingTickets): ?>
<div class="vstack gap-3">
<?php foreach ($waitingTickets as $ticket): ?>
<div class="border rounded p-3 bg-light">
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-3">
<div>
<div class="fw-bold fs-5 text-dark"><?= qh_h($ticket['ticket_number']) ?></div>
<div class="fw-semibold mt-1 text-dark"><?= qh_h($ticket['patient_name']) ?></div>
<div class="small text-muted mt-1"><?= qh_h(qh_name($ticket, 'clinic_name')) ?> &bull; <?= qh_h(qh_name($ticket, 'doctor_name')) ?> &bull; <?= qh_h(qh_t('Rm', 'غرفة')) ?> <?= qh_h($ticket['doctor_room'] ?? '--') ?></div>
</div>
<div class="d-flex gap-2 flex-wrap align-items-center">
<?= qh_status_badge($ticket['status']) ?>
</div>
</div>
<form method="post" class="row g-3 align-items-end">
<input type="hidden" name="ticket_id" value="<?= qh_h((string) $ticket['id']) ?>">
<div class="col-lg-8">
<label class="form-label text-dark fw-semibold small"><?= qh_h(qh_t('Vitals note', 'ملاحظة العلامات الحيوية')) ?></label>
<textarea class="form-control bg-white" name="vitals_notes" rows="2" placeholder="<?= qh_h(qh_t('Blood pressure, pulse, temperature...', 'الضغط والنبض والحرارة...')) ?>" required></textarea>
</div>
<div class="col-lg-4 d-flex flex-column gap-2">
<button class="btn btn-<?= $ticket['status'] === 'waiting_vitals' ? 'primary' : 'warning' ?> shadow-sm" type="submit" name="action" value="call_ticket" formnovalidate><i class="bi bi-megaphone me-1"></i><?= qh_h($ticket['status'] === 'waiting_vitals' ? qh_t('Call', 'نداء') : qh_t('Recall', 'إعادة النداء')) ?></button>
<button class="btn btn-success shadow-sm" type="submit" name="action" value="send"><i class="bi bi-check2-circle me-1"></i><?= qh_h(qh_t('Send to Doctor', 'إرسال للطبيب')) ?></button>
</div>
</form>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="text-center py-5 text-muted">
<p class="mb-0"><?= qh_h(qh_t('No patients are waiting for vitals.', 'لا يوجد مرضى بانتظار العلامات الحيوية.')) ?></p>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php qh_page_end(); ?>

0
output.html Normal file
View File

38
patch_css.php Normal file
View File

@ -0,0 +1,38 @@
<?php
$css = file_get_contents('assets/css/custom.css');
$search = <<<CSS
.announcement-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.2rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface-muted);
}
CSS;
$replace = <<<CSS
.announcement-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 1rem 1.2rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface-muted);
}
CSS;
$newCss = str_replace($search, $replace, $css);
if ($newCss !== $css) {
file_put_contents('assets/css/custom.css', $newCss);
echo "CSS modified.\n";
} else {
echo "CSS not found.\n";
}

18
patch_css_media.php Normal file
View File

@ -0,0 +1,18 @@
<?php
$css = file_get_contents('assets/css/custom.css');
$css = str_replace(
" .queue-row-head,\n .announcement-card,\n .call-strip,",
" .queue-row-head,\n .call-strip,",
$css
);
$css = str_replace(
"html[dir=\"rtl\"] .call-strip,\nhtml[dir=\"rtl\"] .announcement-card,\nhtml[dir=\"rtl\"] .admin-sidebar-link",
"html[dir=\"rtl\"] .call-strip,\nhtml[dir=\"rtl\"] .admin-sidebar-link",
$css
);
file_put_contents('assets/css/custom.css', $css);
echo "CSS RTL and Mobile media queries modified for announcement-card.\n";

36
patch_display.php Normal file
View File

@ -0,0 +1,36 @@
<?php
$content = file_get_contents('display.php');
$search = <<<'HTML'
<div class="card-body text-center p-2">
<div class="fs-2 fw-bold text-primary mb-1"><?= qh_h($ticket['ticket_number']) ?></div>
<div class="small text-muted mb-1 text-truncate" style="max-width: 100%;" title="<?= qh_h(qh_name($ticket, 'doctor_name', qh_t('Doctor', 'الطبيب'))) ?>"><?= qh_h(qh_name($ticket, 'doctor_name', qh_t('Doctor', 'الطبيب'))) ?></div>
<div class="d-inline-block bg-primary text-white rounded px-2 py-1 small fw-bold mt-1">
<?= qh_h(qh_t('Room', 'غرفة')) ?> <?= qh_h($ticket['doctor_room'] ?? '--') ?>
</div>
</div>
<div class="card-footer bg-white text-center text-muted small py-1" style="font-size: 0.65rem;">
<?= qh_format_datetime($ticket['called_at'] ?? $ticket['updated_at']) ?>
</div>
HTML;
$replace = <<<'HTML'
<div class="card-body text-center p-2 d-flex flex-column align-items-center justify-content-center">
<div class="fs-2 fw-bold text-primary mb-1"><?= qh_h($ticket['ticket_number']) ?></div>
<div class="small text-muted mb-1 text-truncate w-100" title="<?= qh_h(qh_name($ticket, 'doctor_name', qh_t('Doctor', 'الطبيب'))) ?>"><?= qh_h(qh_name($ticket, 'doctor_name', qh_t('Doctor', 'الطبيب'))) ?></div>
<div class="bg-primary text-white rounded px-2 py-1 small fw-bold mt-1 mb-2">
<?= qh_h(qh_t('Room', 'غرفة')) ?> <?= qh_h($ticket['doctor_room'] ?? '--') ?>
</div>
<div class="text-muted mt-auto" style="font-size: 0.65rem;">
<?= qh_format_datetime($ticket['called_at'] ?? $ticket['updated_at']) ?>
</div>
</div>
HTML;
$newContent = str_replace($search, $replace, $content);
if ($newContent !== $content) {
file_put_contents('display.php', $newContent);
echo "Replaced successfully\n";
} else {
echo "Pattern not found\n";
}

12
patch_display_w100.php Normal file
View File

@ -0,0 +1,12 @@
<?php
$content = file_get_contents('display.php');
$search = '<div class="card-body text-center p-2 d-flex flex-column align-items-center justify-content-center">';
$replace = '<div class="card-body w-100 text-center p-2 d-flex flex-column align-items-center justify-content-center">';
$newContent = str_replace($search, $replace, $content);
if ($newContent !== $content) {
file_put_contents('display.php', $newContent);
echo "Added w-100 to card-body\n";
} else {
echo "w-100 already added or not found\n";
}

1477
queue_bootstrap.php Normal file

File diff suppressed because it is too large Load Diff

170
reception.php Normal file
View File

@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/queue_bootstrap.php';
qh_boot();
qh_reception_handle_request();
$clinics = qh_fetch_clinics();
$doctors = qh_fetch_doctors();
$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);
qh_page_start(
'reception',
qh_t('Reception', 'الاستقبال'),
qh_t('Issue new tickets and manage patient flow.', 'إصدار التذاكر الجديدة وإدارة حركة المرضى.')
);
?>
<div class="container-fluid container-xxl px-3 px-lg-4">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-4">
<div>
<h1 class="h3 mb-0 text-gray-800 fw-bold"><?= qh_h(qh_t('Reception', 'الاستقبال')) ?></h1>
<p class="text-muted mb-0 mt-1"><?= qh_h(qh_t('Issue new tickets and manage patient flow.', 'إصدار التذاكر الجديدة وإدارة حركة المرضى.')) ?></p>
</div>
</div>
<div class="row g-4">
<div class="col-xl-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white border-bottom py-3">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Issue New Ticket', 'إصدار تذكرة جديدة')) ?></h5>
</div>
<div class="card-body">
<form method="post" class="vstack gap-3" novalidate>
<div>
<label class="form-label fw-semibold text-dark"><?= qh_h(qh_t('Patient name', 'اسم المريض')) ?></label>
<input class="form-control" type="text" name="patient_name" placeholder="Maha Ali" required>
</div>
<div>
<label class="form-label fw-semibold text-dark"><?= qh_h(qh_t('Clinic', 'العيادة')) ?></label>
<select class="form-select js-clinic-select" name="clinic_id" required>
<option value=""><?= qh_h(qh_t('Choose clinic', 'اختر العيادة')) ?></option>
<?php foreach ($clinics as $clinic): ?>
<option value="<?= qh_h((string) $clinic['id']) ?>"><?= qh_h(qh_name($clinic)) ?><?php if ((int) $clinic['requires_vitals'] === 1): ?> &middot; <?= qh_h(qh_t('Vitals first', 'العلامات أولاً')) ?><?php endif; ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label fw-semibold text-dark"><?= qh_h(qh_t('Doctor', 'الطبيب')) ?></label>
<select class="form-select js-doctor-select" name="doctor_id" required>
<option value=""><?= qh_h(qh_t('Choose doctor', 'اختر الطبيب')) ?></option>
<?php foreach ($doctors as $doctor): ?>
<option value="<?= qh_h((string) $doctor['id']) ?>" data-clinic-id="<?= qh_h((string) $doctor['clinic_id']) ?>"><?= qh_h(qh_name($doctor)) ?> &middot; <?= qh_h(qh_t('Rm', 'غرفة')) ?> <?= qh_h($doctor['room_number']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label fw-semibold text-dark"><?= qh_h(qh_t('Preferred language', 'اللغة المفضلة')) ?></label>
<select class="form-select" name="language_pref">
<option value="en">English</option>
<option value="ar">العربية</option>
</select>
</div>
<button class="btn btn-primary mt-2 shadow-sm" type="submit"><?= qh_h(qh_t('Issue ticket', 'إصدار التذكرة')) ?></button>
</form>
</div>
</div>
</div>
<div class="col-xl-8">
<?php if ($currentTicket): ?>
<div class="card shadow-sm border-0 mb-4 bg-light border-start border-primary border-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap">
<div>
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1 small"><?= qh_h(qh_t('Ticket Issued Successfully', 'تم إصدار التذكرة بنجاح')) ?></div>
<div class="display-6 fw-bold mt-2 text-dark"><?= qh_h($currentTicket['ticket_number']) ?></div>
<div class="mt-2 fw-semibold fs-5 text-dark"><?= qh_h($currentTicket['patient_name']) ?></div>
<div class="text-muted mt-1"><?= qh_h(qh_name($currentTicket, 'clinic_name')) ?> &bull; <?= qh_h(qh_name($currentTicket, 'doctor_name')) ?> &bull; <?= qh_h(qh_t('Rm', 'غرفة')) ?> <?= qh_h($currentTicket['doctor_room'] ?? '--') ?></div>
</div>
<div class="d-flex flex-column align-items-lg-end gap-2">
<?= qh_status_badge($currentTicket['status']) ?>
<button class="btn btn-outline-secondary bg-white btn-sm js-print-ticket shadow-sm mt-2" type="button"><i class="bi bi-printer me-1"></i><?= qh_h(qh_t('Print', 'طباعة')) ?></button>
</div>
</div>
<hr class="my-3">
<div class="row g-3 small text-muted">
<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><?= qh_h(qh_t('Language', 'اللغة')) ?>:</strong><br><?= qh_h(qh_locale_label($currentTicket['language_pref'] ?? 'en')) ?></div>
<div class="col-md-4 text-primary"><strong><?= qh_h(qh_t('Next stop', 'المحطة التالية')) ?>:</strong><br><?= qh_h(qh_ticket_next_stop($currentTicket)) ?></div>
</div>
</div>
</div>
<?php endif; ?>
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Today\'s Tickets', 'تذاكر اليوم')) ?></h5>
</div>
<div class="card-body p-0">
<?php if (empty($todayTickets)):
?><div class="text-center py-5 text-muted">
<p class="mb-0"><?= qh_h(qh_t('No tickets issued today.', 'لا توجد تذاكر صادرة اليوم.')) ?></p>
</div>
<?php else:
?><div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="border-0 px-4 py-3"><?= qh_h(qh_t('Ticket', 'التذكرة')) ?></th>
<th class="border-0 py-3"><?= qh_h(qh_t('Patient', 'المريض')) ?></th>
<th class="border-0 py-3"><?= qh_h(qh_t('Clinic', 'العيادة')) ?></th>
<th class="border-0 py-3"><?= qh_h(qh_t('Status', 'الحالة')) ?></th>
<th class="border-0 px-4 py-3 text-end"></th>
</tr>
</thead>
<tbody>
<?php foreach ($todayTickets as $ticket):
?><tr >
<td class="fw-bold px-4 py-3 text-dark"><?= qh_h($ticket['ticket_number']) ?></td>
<td class="py-3"><?= qh_h($ticket['patient_name']) ?></td>
<td class="py-3 text-muted"><?= qh_h(qh_name($ticket, 'clinic_name')) ?></td>
<td class="py-3"><?= qh_status_badge($ticket['status']) ?></td>
<td class="text-end px-4 py-3"><a class="btn btn-sm btn-light border shadow-sm" href="<?= qh_h(qh_url('ticket.php', ['id' => (int) $ticket['id']])) ?>"><?= qh_h(qh_t('View', 'عرض')) ?></a></td>
</tr>
<?php endforeach;
?></tbody>
</table>
</div>
<?php endif;
?></div>
</div>
</div>
</div>
</div>
<?php if ($currentTicket): ?>
<div id="print-ticket" class="d-none d-print-block">
<div class="print-ticket-inner">
<div class="pt-header">
<?= qh_h(qh_hospital_name()) ?>
</div>
<div class="pt-token-title">
<?= qh_h(qh_t("Token No.", "رقم التذكرة")) ?>
</div>
<div class="pt-token-number">
<?= qh_h($currentTicket["ticket_number"]) ?>
</div>
<div class="pt-patient-name">
<?= qh_h($currentTicket["patient_name"]) ?>
</div>
<div class="pt-divider"></div>
<div class="pt-detail">
<span class="pt-label"><?= qh_h(qh_t("Doctor", "الطبيب")) ?>:</span> <?= qh_h(qh_name($currentTicket, "doctor_name")) ?>
</div>
<div class="pt-detail">
<span class="pt-label"><?= qh_h(qh_t("Room", "الغرفة")) ?>:</span> <?= qh_h($currentTicket["doctor_room"] ?? "--") ?>
</div>
<div class="pt-detail">
<span class="pt-label"><?= qh_h(qh_t("Waiting ahead", "أمامك في الطابور")) ?>:</span> <strong><?= qh_get_waiting_count_for_ticket($currentTicket) ?></strong>
</div>
<div class="pt-detail pt-datetime">
<span class="pt-label"><?= qh_h(qh_t("Time", "الوقت")) ?>:</span> <span><?= qh_format_datetime($currentTicket["created_at"]) ?></span>
</div>
<div class="pt-divider"></div>
<div class="pt-footer">
<?= qh_h(qh_t("Please wait for your turn.", "يرجى الانتظار حتى يحين دورك.")) ?>
</div>
</div>
</div>
<?php endif; ?>
<?php qh_page_end(); ?>

118
ticket.php Normal file
View File

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/queue_bootstrap.php';
qh_boot();
$ticketId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
$ticket = $ticketId > 0 ? qh_fetch_ticket($ticketId) : null;
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">
<section class="page-header-panel mb-4">
<div>
<span class="section-kicker"><?= qh_h(qh_t('Ticket detail', 'تفاصيل التذكرة')) ?></span>
<div class="locale-chip mt-3"><?= qh_h(qh_current_language_badge()) ?></div>
<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>
</section>
<?php if (!$ticket): ?>
<div class="panel-card">
<div class="empty-state">
<strong><?= qh_h(qh_t('Ticket not found.', 'لم يتم العثور على التذكرة.')) ?></strong>
<span><?= qh_h(qh_t('Return to reception and choose a valid ticket.', 'عد إلى الاستقبال واختر تذكرة صالحة.')) ?></span>
</div>
</div>
<?php else: ?>
<div class="panel-card mb-4">
<div class="d-flex justify-content-between flex-wrap gap-3 align-items-start">
<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="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 class="d-flex flex-column gap-2 align-items-lg-end">
<?= qh_status_badge($ticket['status']) ?>
<button class="btn btn-outline-secondary bg-white btn-sm js-print-ticket shadow-sm mt-2 mb-2 w-100" type="button"><i class="bi bi-printer me-1"></i><?= qh_h(qh_t("Print Ticket", "طباعة التذكرة")) ?></button>
<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 class="row g-4">
<div class="col-lg-7">
<div class="panel-card h-100">
<h2 class="section-title mb-3"><?= qh_h(qh_t('Visit timeline', 'خط سير الزيارة')) ?></h2>
<div class="timeline-list">
<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>
<?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-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>
<?php endif; ?>
<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-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 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>
</div>
</div>
<div class="col-lg-5">
<div class="panel-card h-100">
<h2 class="section-title mb-3"><?= qh_h(qh_t('Details', 'التفاصيل')) ?></h2>
<dl class="detail-list mb-0">
<div><dt><?= qh_h(qh_t('Clinic', 'العيادة')) ?></dt><dd><?= qh_h(qh_name($ticket, 'clinic_name')) ?></dd></div>
<div><dt><?= qh_h(qh_t('Doctor', 'الطبيب')) ?></dt><dd><?= qh_h(qh_name($ticket, 'doctor_name')) ?></dd></div>
<div><dt><?= qh_h(qh_t('Room', 'الغرفة')) ?></dt><dd><?= qh_h($ticket['doctor_room'] ?? '--') ?></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><?= qh_h(qh_t('Last note', 'آخر ملاحظة')) ?></dt><dd><?= qh_h(qh_ticket_last_note($ticket)) ?></dd></div>
</dl>
<div class="d-flex flex-wrap gap-2 mt-4">
<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="<?= 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="<?= qh_h(qh_url('display.php')) ?>"><?= qh_h(qh_t('Open display', 'فتح الشاشة')) ?></a>
</div>
</div>
</div>
</div>
<?php endif; ?>
</div>
<?php if ($ticket): ?>
<div id="print-ticket" class="d-none d-print-block">
<div class="print-ticket-inner">
<div class="pt-header">
<?= qh_h(qh_hospital_name()) ?>
</div>
<div class="pt-token-title">
<?= qh_h(qh_t("Token No.", "رقم التذكرة")) ?>
</div>
<div class="pt-token-number">
<?= qh_h($ticket["ticket_number"]) ?>
</div>
<div class="pt-patient-name">
<?= qh_h($ticket["patient_name"]) ?>
</div>
<div class="pt-divider"></div>
<div class="pt-detail">
<span class="pt-label"><?= qh_h(qh_t("Doctor", "الطبيب")) ?>:</span> <?= qh_h(qh_name($ticket, "doctor_name")) ?>
</div>
<div class="pt-detail">
<span class="pt-label"><?= qh_h(qh_t("Room", "الغرفة")) ?>:</span> <?= qh_h($ticket["doctor_room"] ?? "--") ?>
</div>
<div class="pt-detail">
<span class="pt-label"><?= qh_h(qh_t("Waiting ahead", "أمامك في الطابور")) ?>:</span> <strong><?= qh_get_waiting_count_for_ticket($ticket) ?></strong>
</div>
<div class="pt-detail pt-datetime">
<span class="pt-label"><?= qh_h(qh_t("Time", "الوقت")) ?>:</span> <span><?= qh_format_datetime($ticket["created_at"]) ?></span>
</div>
<div class="pt-divider"></div>
<div class="pt-footer">
<?= qh_h(qh_t("Please wait for your turn.", "يرجى الانتظار حتى يحين دورك.")) ?>
</div>
</div>
</div>
<?php endif; ?>
<?php qh_page_end(); ?>