-
Loading…
+
+
+
+
+
Hospital queue system / نظام الطوابير
+
One bilingual workflow for reception, nursing, doctors, and the public screen.
+
Issue one ticket, route it through optional vitals, call patients to the right room, and announce the latest calls with browser text-to-speech on the general display.
+
+
+
+
+
Today / اليوم
+
+
+
+
= qh_h((string) $stats['issued_today']) ?>
+
Issued / المُصدرة
+
+
+
+
+
= qh_h((string) $stats['waiting_vitals']) ?>
+
Vitals / الحيوية
+
+
+
+
+
= qh_h((string) $stats['ready_for_doctor']) ?>
+
Ready / جاهز
+
+
+
+
+
= qh_h((string) $stats['active_rooms']) ?>
+
Active rooms / الغرف النشطة
+
+
+
+
-
= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.
-
This page will update automatically as the plan is implemented.
-
Runtime: PHP = htmlspecialchars($phpVersion) ?> — UTC = htmlspecialchars($now) ?>
-
-
- Page updated: = htmlspecialchars($now) ?> (UTC)
-
-
-
+
+
+
+
+
+
+
+
Live queue overview / نظرة مباشرة على الطابور
+
Clinic-level demand for vitals, doctor readiness, and active calls.
+
+
Manage clinics & doctors
+
+
+
+
+
+ Clinic / العيادة
+ Vitals wait
+ Doctor wait
+ Active calls
+ Total today
+
+
+
+
+
+
+ = qh_h($row['name_en']) ?>
+ = qh_h($row['name_ar']) ?>
+
+ = qh_h((string) $row['vitals_waiting']) ?>
+ = qh_h((string) $row['doctor_waiting']) ?>
+ = qh_h((string) $row['active_calls']) ?>
+ = qh_h((string) $row['total_today']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
Current calls / النداءات الحالية
+
Patients already called to a doctor room.
+
+
+
+
+
+
+
+
= qh_h($ticket['ticket_number']) ?>
+
= qh_h($ticket['doctor_name_en'] ?? 'Unassigned') ?> · Room = qh_h($ticket['doctor_room'] ?? '--') ?>
+
+ = qh_status_badge($ticket['status']) ?>
+
+
+
+
+
+ No active calls yet.
+ Use the doctor page to call the next patient.
+
+
+
+
+
+
+
+
+
+
+
+
Recent patient flow / آخر حركة للمرضى
+
Each ticket is linked to a detail page showing its current stage.
+
+
Create new ticket
+
+
+
+
+
+
+ Ticket
+ Patient
+ Clinic
+ Doctor / Room
+ Status
+
+
+
+
+
+
+ = qh_h($ticket['ticket_number']) ?>
+
+ = qh_h($ticket['patient_name']) ?>
+ = strtoupper(qh_h($ticket['language_pref'])) ?>
+
+
+ = qh_h($ticket['clinic_name_en'] ?? '—') ?>
+ = qh_h($ticket['clinic_name_ar'] ?? '') ?>
+
+ = qh_h($ticket['doctor_name_en'] ?? '—') ?> · = qh_h($ticket['doctor_room'] ?? '--') ?>
+ = qh_status_badge($ticket['status']) ?>
+ View detail
+
+
+
+
+
+
+
+ No active tickets yet.
+ Start from the reception desk to issue the first patient ticket.
+
+
+
+
+
diff --git a/nursing.php b/nursing.php
new file mode 100644
index 0000000..022aa36
--- /dev/null
+++ b/nursing.php
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
Waiting for vitals / بانتظار العلامات الحيوية
+
Add a short clinical note to transfer the patient to the assigned doctor.
+
+
= qh_h((string) count($waitingTickets)) ?> patients
+
+
+
+
+
+
+
+
+
= qh_h($ticket['ticket_number']) ?>
+
= qh_h($ticket['patient_name']) ?>
+
= qh_h($ticket['clinic_name_en'] ?? '') ?> → = qh_h($ticket['doctor_name_en'] ?? '') ?> · Room = qh_h($ticket['doctor_room'] ?? '--') ?>
+
+
+
+
+
+
+
+
+
+ No patients waiting for vitals.
+ Tickets from vitals-required clinics will appear here automatically after issue.
+
+
+
+
+
diff --git a/queue_bootstrap.php b/queue_bootstrap.php
new file mode 100644
index 0000000..9e8988e
--- /dev/null
+++ b/queue_bootstrap.php
@@ -0,0 +1,715 @@
+exec($sql);
+}
+
+function qh_seed_demo_data(): void
+{
+ $pdo = db();
+
+ $clinicCount = (int) $pdo->query("SELECT COUNT(*) FROM hospital_queue_records WHERE item_type = 'clinic'")->fetchColumn();
+ if ($clinicCount === 0) {
+ $insertClinic = $pdo->prepare(
+ 'INSERT INTO hospital_queue_records (item_type, code, name_en, name_ar, requires_vitals, sort_order, status)
+ VALUES (\'clinic\', :code, :name_en, :name_ar, :requires_vitals, :sort_order, \'active\')'
+ );
+
+ $clinics = [
+ ['code' => 'GEN', 'name_en' => 'General Medicine', 'name_ar' => 'الطب العام', 'requires_vitals' => 1, 'sort_order' => 10],
+ ['code' => 'PED', 'name_en' => 'Pediatrics', 'name_ar' => 'طب الأطفال', 'requires_vitals' => 0, 'sort_order' => 20],
+ ['code' => 'CAR', 'name_en' => 'Cardiology', 'name_ar' => 'أمراض القلب', 'requires_vitals' => 1, 'sort_order' => 30],
+ ];
+
+ foreach ($clinics as $clinic) {
+ $insertClinic->execute($clinic);
+ }
+ }
+
+ $clinicMap = [];
+ foreach (qh_fetch_clinics() as $clinic) {
+ $clinicMap[$clinic['code']] = $clinic['id'];
+ }
+
+ $doctorCount = (int) $pdo->query("SELECT COUNT(*) FROM hospital_queue_records WHERE item_type = 'doctor'")->fetchColumn();
+ if ($doctorCount === 0 && $clinicMap !== []) {
+ $insertDoctor = $pdo->prepare(
+ 'INSERT INTO hospital_queue_records (item_type, name_en, name_ar, clinic_id, room_number, sort_order, status)
+ VALUES (\'doctor\', :name_en, :name_ar, :clinic_id, :room_number, :sort_order, \'active\')'
+ );
+
+ $doctors = [
+ ['name_en' => 'Dr. Sarah Malik', 'name_ar' => 'د. سارة مالك', 'clinic_id' => $clinicMap['GEN'] ?? null, 'room_number' => '201', 'sort_order' => 10],
+ ['name_en' => 'Dr. Omar Nasser', 'name_ar' => 'د. عمر ناصر', 'clinic_id' => $clinicMap['GEN'] ?? null, 'room_number' => '202', 'sort_order' => 20],
+ ['name_en' => 'Dr. Lina Haddad', 'name_ar' => 'د. لينا حداد', 'clinic_id' => $clinicMap['PED'] ?? null, 'room_number' => '103', 'sort_order' => 30],
+ ['name_en' => 'Dr. Ahmad Kareem', 'name_ar' => 'د. أحمد كريم', 'clinic_id' => $clinicMap['CAR'] ?? null, 'room_number' => '305', 'sort_order' => 40],
+ ];
+
+ foreach ($doctors as $doctor) {
+ if ($doctor['clinic_id']) {
+ $insertDoctor->execute($doctor);
+ }
+ }
+ }
+
+ $ticketCount = (int) $pdo->query("SELECT COUNT(*) FROM hospital_queue_records WHERE item_type = 'ticket'")->fetchColumn();
+ if ($ticketCount === 0) {
+ $doctors = qh_fetch_doctors();
+ $doctorByClinic = [];
+ foreach ($doctors as $doctor) {
+ $doctorByClinic[$doctor['clinic_id']][] = $doctor;
+ }
+
+ foreach (qh_fetch_clinics() as $clinic) {
+ if (empty($doctorByClinic[$clinic['id']])) {
+ continue;
+ }
+ $assignedDoctor = $doctorByClinic[$clinic['id']][0];
+ qh_create_ticket(
+ $clinic['name_en'] === 'General Medicine' ? 'Maha Ali' : 'Yousef Karim',
+ (int) $clinic['id'],
+ (int) $assignedDoctor['id'],
+ $clinic['name_en'] === 'General Medicine' ? 'ar' : 'en',
+ $clinic['requires_vitals'] ? 'waiting_vitals' : 'ready_for_doctor'
+ );
+ if ($clinic['name_en'] !== 'General Medicine') {
+ break;
+ }
+ }
+ }
+}
+
+function qh_h(?string $value): string
+{
+ return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
+}
+
+function qh_project_name(string $fallback = 'Hospital Queue'): string
+{
+ $candidate = trim((string) ($_SERVER['PROJECT_NAME'] ?? $_SERVER['PROJECT_TITLE'] ?? ''));
+ return $candidate !== '' ? $candidate : $fallback;
+}
+
+function qh_project_description(string $fallback = 'Bilingual hospital queue workflow for reception, nursing, doctors, and the public display.'): string
+{
+ $candidate = trim((string) ($_SERVER['PROJECT_DESCRIPTION'] ?? ''));
+ return $candidate !== '' ? $candidate : $fallback;
+}
+
+function qh_asset_version(string $relativePath): int
+{
+ $fullPath = __DIR__ . '/' . ltrim($relativePath, '/');
+ return is_file($fullPath) ? (int) filemtime($fullPath) : time();
+}
+
+function qh_label(string $en, string $ar, string $wrapper = 'span'): string
+{
+ $tag = preg_replace('/[^a-z0-9]/i', '', $wrapper) ?: 'span';
+ return sprintf(
+ '<%1$s class="bi-label">
%2$s / %3$s %1$s>',
+ $tag,
+ qh_h($en),
+ qh_h($ar)
+ );
+}
+
+function qh_page_start(string $activePage, string $pageTitle, string $metaDescription = ''): void
+{
+ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? qh_project_description();
+ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
+ $fullTitle = $pageTitle . ' · ' . qh_project_name();
+ $description = $metaDescription !== '' ? $metaDescription : qh_project_description();
+ $bodyClass = 'page-' . preg_replace('/[^a-z0-9\-]/i', '-', $activePage);
+ $assetVersionCss = qh_asset_version('assets/css/custom.css');
+ $assetVersionJs = qh_asset_version('assets/js/main.js');
+
+ echo '';
+ echo '';
+ echo '';
+ echo '
';
+ echo '
';
+ echo '
' . qh_h($fullTitle) . ' ';
+ echo '
';
+ if ($projectDescription) {
+ echo '
';
+ echo '
';
+ }
+ if ($projectImageUrl) {
+ echo '
';
+ echo '
';
+ }
+ echo '
';
+ echo '
';
+ echo '
';
+ echo '';
+ echo '';
+ if ($activePage !== 'display') {
+ qh_render_nav($activePage);
+ }
+ echo '
';
+ qh_render_flash();
+}
+
+function qh_page_end(): void
+{
+ $assetVersionJs = qh_asset_version('assets/js/main.js');
+ echo ' ';
+ echo '';
+ echo '';
+ echo '';
+}
+
+function qh_render_nav(string $activePage): void
+{
+ $links = [
+ 'home' => ['href' => 'index.php', 'label' => qh_label('Operations', 'العمليات')],
+ 'admin' => ['href' => 'admin.php', 'label' => qh_label('Admin', 'الإدارة')],
+ 'reception' => ['href' => 'reception.php', 'label' => qh_label('Reception', 'الاستقبال')],
+ 'nursing' => ['href' => 'nursing.php', 'label' => qh_label('Nursing', 'التمريض')],
+ 'doctor' => ['href' => 'doctor.php', 'label' => qh_label('Doctor', 'الطبيب')],
+ 'display' => ['href' => 'display.php', 'label' => qh_label('Display', 'الشاشة العامة')],
+ ];
+
+ echo '
';
+ echo ' ';
+ echo ' ';
+ echo ' ';
+ echo ' ';
+}
+
+function qh_set_flash(string $type, string $message): void
+{
+ $_SESSION['flash'] = ['type' => $type, 'message' => $message];
+}
+
+function qh_render_flash(): void
+{
+ if (empty($_SESSION['flash']) || !is_array($_SESSION['flash'])) {
+ return;
+ }
+
+ $flash = $_SESSION['flash'];
+ unset($_SESSION['flash']);
+
+ $typeMap = [
+ 'success' => 'success',
+ 'danger' => 'danger',
+ 'warning' => 'warning',
+ 'info' => 'primary',
+ ];
+ $toastType = $typeMap[$flash['type']] ?? 'primary';
+
+ echo '
';
+ echo '
';
+ echo '
';
+ echo '
' . qh_h((string) $flash['message']) . '
';
+ echo '
';
+ echo '
';
+ echo '
';
+ echo '
';
+}
+
+function qh_redirect(string $location): void
+{
+ header('Location: ' . $location);
+ exit;
+}
+
+function qh_fetch_clinics(): array
+{
+ $stmt = db()->query("SELECT * FROM hospital_queue_records WHERE item_type = 'clinic' ORDER BY sort_order ASC, name_en ASC");
+ return $stmt->fetchAll();
+}
+
+function qh_fetch_doctors(?int $clinicId = null): array
+{
+ if ($clinicId) {
+ $stmt = db()->prepare(
+ "SELECT d.*, c.name_en AS clinic_name_en, c.name_ar AS clinic_name_ar, c.code AS clinic_code
+ FROM hospital_queue_records d
+ LEFT JOIN hospital_queue_records c ON c.id = d.clinic_id AND c.item_type = 'clinic'
+ WHERE d.item_type = 'doctor' AND d.clinic_id = :clinic_id
+ ORDER BY d.sort_order ASC, d.name_en ASC"
+ );
+ $stmt->execute(['clinic_id' => $clinicId]);
+ return $stmt->fetchAll();
+ }
+
+ $stmt = db()->query(
+ "SELECT d.*, c.name_en AS clinic_name_en, c.name_ar AS clinic_name_ar, c.code AS clinic_code
+ FROM hospital_queue_records d
+ LEFT JOIN hospital_queue_records c ON c.id = d.clinic_id AND c.item_type = 'clinic'
+ WHERE d.item_type = 'doctor'
+ ORDER BY c.sort_order ASC, d.sort_order ASC, d.name_en ASC"
+ );
+ return $stmt->fetchAll();
+}
+
+function qh_fetch_ticket(int $ticketId): ?array
+{
+ $stmt = db()->prepare(
+ "SELECT t.*, c.name_en AS clinic_name_en, c.name_ar AS clinic_name_ar, c.code AS clinic_code, c.requires_vitals AS clinic_requires_vitals,
+ d.name_en AS doctor_name_en, d.name_ar AS doctor_name_ar, d.room_number AS doctor_room
+ FROM hospital_queue_records t
+ LEFT JOIN hospital_queue_records c ON c.id = t.clinic_id AND c.item_type = 'clinic'
+ LEFT JOIN hospital_queue_records d ON d.id = t.doctor_id AND d.item_type = 'doctor'
+ WHERE t.item_type = 'ticket' AND t.id = :ticket_id
+ LIMIT 1"
+ );
+ $stmt->execute(['ticket_id' => $ticketId]);
+ $ticket = $stmt->fetch();
+ return $ticket ?: null;
+}
+
+function qh_fetch_tickets(array $statuses = [], ?int $doctorId = null, ?int $limit = null): array
+{
+ $sql = "SELECT t.*, c.name_en AS clinic_name_en, c.name_ar AS clinic_name_ar, c.code AS clinic_code, c.requires_vitals AS clinic_requires_vitals,
+ d.name_en AS doctor_name_en, d.name_ar AS doctor_name_ar, d.room_number AS doctor_room
+ FROM hospital_queue_records t
+ LEFT JOIN hospital_queue_records c ON c.id = t.clinic_id AND c.item_type = 'clinic'
+ LEFT JOIN hospital_queue_records d ON d.id = t.doctor_id AND d.item_type = 'doctor'
+ WHERE t.item_type = 'ticket'";
+
+ $params = [];
+
+ if ($statuses !== []) {
+ $statusPlaceholders = [];
+ foreach ($statuses as $index => $status) {
+ $key = 'status_' . $index;
+ $statusPlaceholders[] = ':' . $key;
+ $params[$key] = $status;
+ }
+ $sql .= ' AND t.status IN (' . implode(', ', $statusPlaceholders) . ')';
+ }
+
+ if ($doctorId) {
+ $sql .= ' AND t.doctor_id = :doctor_id';
+ $params['doctor_id'] = $doctorId;
+ }
+
+ $sql .= ' ORDER BY COALESCE(t.called_at, t.created_at) DESC, t.id DESC';
+
+ if ($limit) {
+ $sql .= ' LIMIT ' . (int) $limit;
+ }
+
+ $stmt = db()->prepare($sql);
+ $stmt->execute($params);
+ return $stmt->fetchAll();
+}
+
+function qh_queue_overview(): array
+{
+ $sql = "SELECT c.id, c.name_en, c.name_ar, c.code,
+ SUM(CASE WHEN t.status = 'waiting_vitals' THEN 1 ELSE 0 END) AS vitals_waiting,
+ SUM(CASE WHEN t.status = 'ready_for_doctor' THEN 1 ELSE 0 END) AS doctor_waiting,
+ SUM(CASE WHEN t.status IN ('called', 'in_progress') THEN 1 ELSE 0 END) AS active_calls,
+ COUNT(t.id) AS total_today
+ FROM hospital_queue_records c
+ LEFT JOIN hospital_queue_records t
+ ON t.clinic_id = c.id
+ AND t.item_type = 'ticket'
+ AND DATE(t.created_at) = CURDATE()
+ WHERE c.item_type = 'clinic'
+ GROUP BY c.id, c.name_en, c.name_ar, c.code
+ ORDER BY c.sort_order ASC, c.name_en ASC";
+
+ return db()->query($sql)->fetchAll();
+}
+
+function qh_dashboard_stats(): array
+{
+ $pdo = db();
+ return [
+ 'issued_today' => (int) $pdo->query("SELECT COUNT(*) FROM hospital_queue_records WHERE item_type = 'ticket' AND DATE(created_at) = CURDATE()")->fetchColumn(),
+ 'waiting_vitals' => (int) $pdo->query("SELECT COUNT(*) FROM hospital_queue_records WHERE item_type = 'ticket' AND status = 'waiting_vitals'")->fetchColumn(),
+ 'ready_for_doctor' => (int) $pdo->query("SELECT COUNT(*) FROM hospital_queue_records WHERE item_type = 'ticket' AND status = 'ready_for_doctor'")->fetchColumn(),
+ 'active_rooms' => (int) $pdo->query("SELECT COUNT(DISTINCT doctor_id) FROM hospital_queue_records WHERE item_type = 'ticket' AND status IN ('called', 'in_progress') AND doctor_id IS NOT NULL")->fetchColumn(),
+ ];
+}
+
+function qh_create_ticket(string $patientName, int $clinicId, int $doctorId, string $languagePref = 'en', ?string $forcedStatus = null): int
+{
+ $pdo = db();
+ $clinic = qh_fetch_clinic($clinicId);
+ if (!$clinic) {
+ throw new RuntimeException('Clinic not found.');
+ }
+
+ $doctor = qh_fetch_doctor($doctorId);
+ if (!$doctor || (int) $doctor['clinic_id'] !== $clinicId) {
+ throw new RuntimeException('Doctor does not belong to the selected clinic.');
+ }
+
+ $ticketNumber = qh_generate_ticket_number($clinic['code']);
+ $status = $forcedStatus ?: ((int) $clinic['requires_vitals'] === 1 ? 'waiting_vitals' : 'ready_for_doctor');
+
+ $stmt = $pdo->prepare(
+ "INSERT INTO hospital_queue_records
+ (item_type, clinic_id, doctor_id, patient_name, language_pref, ticket_number, status, display_note)
+ VALUES
+ ('ticket', :clinic_id, :doctor_id, :patient_name, :language_pref, :ticket_number, :status, :display_note)"
+ );
+ $stmt->execute([
+ 'clinic_id' => $clinicId,
+ 'doctor_id' => $doctorId,
+ 'patient_name' => $patientName,
+ 'language_pref' => in_array($languagePref, ['en', 'ar'], true) ? $languagePref : 'en',
+ 'ticket_number' => $ticketNumber,
+ 'status' => $status,
+ 'display_note' => $status === 'waiting_vitals'
+ ? 'Proceed to nursing vitals first.'
+ : 'Wait for your doctor call on the public screen.',
+ ]);
+
+ return (int) $pdo->lastInsertId();
+}
+
+function qh_generate_ticket_number(string $clinicCode): string
+{
+ $prefix = strtoupper(substr(preg_replace('/[^A-Z0-9]/i', '', $clinicCode), 0, 3));
+ $stmt = db()->prepare(
+ "SELECT COUNT(*)
+ FROM hospital_queue_records
+ WHERE item_type = 'ticket'
+ AND ticket_number LIKE :prefix"
+ );
+ $stmt->execute(['prefix' => $prefix . '-%']);
+ $next = ((int) $stmt->fetchColumn()) + 1;
+ return $prefix . '-' . str_pad((string) $next, 3, '0', STR_PAD_LEFT);
+}
+
+function qh_fetch_clinic(int $clinicId): ?array
+{
+ $stmt = db()->prepare("SELECT * FROM hospital_queue_records WHERE item_type = 'clinic' AND id = :id LIMIT 1");
+ $stmt->execute(['id' => $clinicId]);
+ $row = $stmt->fetch();
+ return $row ?: null;
+}
+
+function qh_fetch_doctor(int $doctorId): ?array
+{
+ $stmt = db()->prepare("SELECT * FROM hospital_queue_records WHERE item_type = 'doctor' AND id = :id LIMIT 1");
+ $stmt->execute(['id' => $doctorId]);
+ $row = $stmt->fetch();
+ return $row ?: null;
+}
+
+function qh_status_meta(string $status): array
+{
+ $map = [
+ 'waiting_vitals' => ['class' => 'warning', 'en' => 'Waiting for vitals', 'ar' => 'بانتظار العلامات الحيوية'],
+ 'ready_for_doctor' => ['class' => 'info', 'en' => 'Ready for doctor', 'ar' => 'جاهز للطبيب'],
+ 'called' => ['class' => 'primary', 'en' => 'Called', 'ar' => 'تم النداء'],
+ 'in_progress' => ['class' => 'secondary', 'en' => 'In consultation', 'ar' => 'داخل العيادة'],
+ 'done' => ['class' => 'success', 'en' => 'Completed', 'ar' => 'مكتمل'],
+ 'no_show' => ['class' => 'danger', 'en' => 'No-show', 'ar' => 'لم يحضر'],
+ ];
+
+ return $map[$status] ?? ['class' => 'light', 'en' => ucfirst(str_replace('_', ' ', $status)), 'ar' => $status];
+}
+
+function qh_status_badge(string $status): string
+{
+ $meta = qh_status_meta($status);
+ return '
' . qh_label($meta['en'], $meta['ar']) . ' ';
+}
+
+function qh_call_message(array $ticket): array
+{
+ $ticketNumber = $ticket['ticket_number'] ?? '---';
+ $doctorNameEn = $ticket['doctor_name_en'] ?? 'Doctor';
+ $doctorNameAr = $ticket['doctor_name_ar'] ?? 'الطبيب';
+ $room = $ticket['doctor_room'] ?? '--';
+
+ return [
+ 'en' => sprintf('Ticket %s, please proceed to room %s for %s.', $ticketNumber, $room, $doctorNameEn),
+ 'ar' => sprintf('رقم التذكرة %s، يرجى التوجه إلى الغرفة %s إلى %s.', $ticketNumber, $room, $doctorNameAr),
+ ];
+}
+
+function qh_format_datetime(?string $value): string
+{
+ if (!$value) {
+ return '—';
+ }
+ $timestamp = strtotime($value);
+ return $timestamp ? date('M d, Y H:i', $timestamp) : '—';
+}
+
+function qh_require_post(): void
+{
+ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
+ qh_set_flash('danger', 'Invalid request method.');
+ qh_redirect('index.php');
+ }
+}
+
+function qh_admin_handle_request(): void
+{
+ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
+ return;
+ }
+
+ $action = $_POST['action'] ?? '';
+ $pdo = db();
+
+ try {
+ if ($action === 'add_clinic') {
+ $code = strtoupper(trim((string) ($_POST['code'] ?? '')));
+ $nameEn = trim((string) ($_POST['name_en'] ?? ''));
+ $nameAr = trim((string) ($_POST['name_ar'] ?? ''));
+ $requiresVitals = isset($_POST['requires_vitals']) ? 1 : 0;
+ if ($code === '' || $nameEn === '' || $nameAr === '') {
+ throw new InvalidArgumentException('Please complete the clinic code and bilingual names.');
+ }
+ $stmt = $pdo->prepare(
+ "INSERT INTO hospital_queue_records (item_type, code, name_en, name_ar, requires_vitals, sort_order, status)
+ VALUES ('clinic', :code, :name_en, :name_ar, :requires_vitals, :sort_order, 'active')"
+ );
+ $stmt->execute([
+ 'code' => substr($code, 0, 10),
+ 'name_en' => $nameEn,
+ 'name_ar' => $nameAr,
+ 'requires_vitals' => $requiresVitals,
+ 'sort_order' => (int) ($_POST['sort_order'] ?? 50),
+ ]);
+ qh_set_flash('success', 'Clinic saved successfully.');
+ } elseif ($action === 'update_clinic') {
+ $clinicId = (int) ($_POST['clinic_id'] ?? 0);
+ $stmt = $pdo->prepare(
+ "UPDATE hospital_queue_records
+ SET requires_vitals = :requires_vitals, sort_order = :sort_order
+ WHERE item_type = 'clinic' AND id = :clinic_id"
+ );
+ $stmt->execute([
+ 'requires_vitals' => isset($_POST['requires_vitals']) ? 1 : 0,
+ 'sort_order' => (int) ($_POST['sort_order'] ?? 50),
+ 'clinic_id' => $clinicId,
+ ]);
+ qh_set_flash('success', 'Clinic settings updated.');
+ } elseif ($action === 'add_doctor') {
+ $nameEn = trim((string) ($_POST['name_en'] ?? ''));
+ $nameAr = trim((string) ($_POST['name_ar'] ?? ''));
+ $clinicId = (int) ($_POST['clinic_id'] ?? 0);
+ $roomNumber = trim((string) ($_POST['room_number'] ?? ''));
+ if ($nameEn === '' || $nameAr === '' || $clinicId <= 0 || $roomNumber === '') {
+ throw new InvalidArgumentException('Please complete the doctor form before saving.');
+ }
+ $stmt = $pdo->prepare(
+ "INSERT INTO hospital_queue_records (item_type, name_en, name_ar, clinic_id, room_number, sort_order, status)
+ VALUES ('doctor', :name_en, :name_ar, :clinic_id, :room_number, :sort_order, 'active')"
+ );
+ $stmt->execute([
+ 'name_en' => $nameEn,
+ 'name_ar' => $nameAr,
+ 'clinic_id' => $clinicId,
+ 'room_number' => $roomNumber,
+ 'sort_order' => (int) ($_POST['sort_order'] ?? 50),
+ ]);
+ qh_set_flash('success', 'Doctor profile saved.');
+ } elseif ($action === 'update_doctor') {
+ $doctorId = (int) ($_POST['doctor_id'] ?? 0);
+ $clinicId = (int) ($_POST['clinic_id'] ?? 0);
+ $roomNumber = trim((string) ($_POST['room_number'] ?? ''));
+ $stmt = $pdo->prepare(
+ "UPDATE hospital_queue_records
+ SET clinic_id = :clinic_id, room_number = :room_number, sort_order = :sort_order
+ WHERE item_type = 'doctor' AND id = :doctor_id"
+ );
+ $stmt->execute([
+ 'clinic_id' => $clinicId,
+ 'room_number' => $roomNumber,
+ 'sort_order' => (int) ($_POST['sort_order'] ?? 50),
+ 'doctor_id' => $doctorId,
+ ]);
+ qh_set_flash('success', 'Doctor assignment updated.');
+ }
+ } catch (Throwable $exception) {
+ qh_set_flash('danger', $exception->getMessage());
+ }
+
+ qh_redirect('admin.php');
+}
+
+function qh_reception_handle_request(): void
+{
+ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
+ return;
+ }
+
+ try {
+ $patientName = trim((string) ($_POST['patient_name'] ?? ''));
+ $clinicId = (int) ($_POST['clinic_id'] ?? 0);
+ $doctorId = (int) ($_POST['doctor_id'] ?? 0);
+ $languagePref = trim((string) ($_POST['language_pref'] ?? 'en'));
+
+ if ($patientName === '' || $clinicId <= 0 || $doctorId <= 0) {
+ throw new InvalidArgumentException('Please complete patient name, clinic, and doctor.');
+ }
+
+ $ticketId = qh_create_ticket($patientName, $clinicId, $doctorId, $languagePref);
+ qh_set_flash('success', 'Ticket issued successfully.');
+ qh_redirect('reception.php?ticket_id=' . $ticketId);
+ } catch (Throwable $exception) {
+ qh_set_flash('danger', $exception->getMessage());
+ qh_redirect('reception.php');
+ }
+}
+
+function qh_nursing_handle_request(): void
+{
+ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
+ return;
+ }
+
+ try {
+ $ticketId = (int) ($_POST['ticket_id'] ?? 0);
+ $vitalsNotes = trim((string) ($_POST['vitals_notes'] ?? ''));
+ if ($ticketId <= 0 || $vitalsNotes === '') {
+ throw new InvalidArgumentException('Please add a short vitals note before sending the patient forward.');
+ }
+
+ $stmt = db()->prepare(
+ "UPDATE hospital_queue_records
+ SET vitals_notes = :vitals_notes,
+ status = 'ready_for_doctor',
+ display_note = 'Vitals completed. Wait for doctor call.'
+ WHERE item_type = 'ticket' AND id = :ticket_id AND status = 'waiting_vitals'"
+ );
+ $stmt->execute([
+ 'vitals_notes' => $vitalsNotes,
+ 'ticket_id' => $ticketId,
+ ]);
+ qh_set_flash('success', 'Vitals captured and patient moved to doctor queue.');
+ } catch (Throwable $exception) {
+ qh_set_flash('danger', $exception->getMessage());
+ }
+
+ qh_redirect('nursing.php');
+}
+
+function qh_doctor_handle_request(): void
+{
+ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
+ return;
+ }
+
+ try {
+ $ticketId = (int) ($_POST['ticket_id'] ?? 0);
+ $doctorId = (int) ($_POST['doctor_id'] ?? 0);
+ $action = trim((string) ($_POST['action'] ?? ''));
+ $ticket = qh_fetch_ticket($ticketId);
+ if (!$ticket || $doctorId <= 0 || (int) $ticket['doctor_id'] !== $doctorId) {
+ throw new InvalidArgumentException('That ticket is not available for the selected doctor.');
+ }
+
+ if ($action === 'call_ticket') {
+ $message = qh_call_message($ticket);
+ $stmt = db()->prepare(
+ "UPDATE hospital_queue_records
+ SET status = 'called', called_at = NOW(), display_note = :display_note
+ WHERE item_type = 'ticket' AND id = :ticket_id"
+ );
+ $stmt->execute([
+ 'display_note' => $message['en'],
+ 'ticket_id' => $ticketId,
+ ]);
+ qh_set_flash('success', 'Patient call pushed to the public display.');
+ } elseif ($action === 'start_visit') {
+ $stmt = db()->prepare(
+ "UPDATE hospital_queue_records
+ SET status = 'in_progress', display_note = 'Patient is now in consultation.'
+ WHERE item_type = 'ticket' AND id = :ticket_id"
+ );
+ $stmt->execute(['ticket_id' => $ticketId]);
+ qh_set_flash('success', 'Consultation started.');
+ } elseif ($action === 'complete_ticket') {
+ $stmt = db()->prepare(
+ "UPDATE hospital_queue_records
+ SET status = 'done', served_at = NOW(), display_note = 'Visit completed.'
+ WHERE item_type = 'ticket' AND id = :ticket_id"
+ );
+ $stmt->execute(['ticket_id' => $ticketId]);
+ qh_set_flash('success', 'Visit marked as completed.');
+ } elseif ($action === 'mark_no_show') {
+ $stmt = db()->prepare(
+ "UPDATE hospital_queue_records
+ SET status = 'no_show', served_at = NOW(), display_note = 'Marked as no-show.'
+ WHERE item_type = 'ticket' AND id = :ticket_id"
+ );
+ $stmt->execute(['ticket_id' => $ticketId]);
+ qh_set_flash('warning', 'Patient marked as no-show.');
+ }
+
+ qh_redirect('doctor.php?doctor_id=' . $doctorId);
+ } catch (Throwable $exception) {
+ qh_set_flash('danger', $exception->getMessage());
+ qh_redirect('doctor.php');
+ }
+}
diff --git a/reception.php b/reception.php
new file mode 100644
index 0000000..66e699d
--- /dev/null
+++ b/reception.php
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
New patient ticket / تذكرة مريض جديدة
+
+
+
+
+
+
+
+
+
+
Issued ticket / التذكرة الصادرة
+
= qh_h($currentTicket['ticket_number']) ?>
+
= qh_h($currentTicket['patient_name']) ?>
+
= qh_h($currentTicket['clinic_name_en'] ?? '') ?> · = qh_h($currentTicket['doctor_name_en'] ?? '') ?> · Room = qh_h($currentTicket['doctor_room'] ?? '--') ?>
+
+
+ = qh_status_badge($currentTicket['status']) ?>
+ Print ticket
+
+
+
+
+
Issued: = qh_format_datetime($currentTicket['created_at']) ?>
+
Language: = strtoupper(qh_h($currentTicket['language_pref'])) ?>
+
Next stop: = (int) $currentTicket['clinic_requires_vitals'] === 1 ? 'Nursing vitals / التمريض' : 'Doctor waiting / انتظار الطبيب' ?>
+
+
+
+
+
+
+
+
Today’s tickets / تذاكر اليوم
+
The latest issued tickets and where they currently are in the visit flow.
+
+
+
+
+
+
+ Ticket
+ Patient
+ Clinic
+ Status
+
+
+
+
+
+
+ = qh_h($ticket['ticket_number']) ?>
+ = qh_h($ticket['patient_name']) ?>
+ = qh_h($ticket['clinic_name_en'] ?? '—') ?>
+ = qh_status_badge($ticket['status']) ?>
+ View
+
+
+
+
+
+
+
+
+
+
diff --git a/ticket.php b/ticket.php
new file mode 100644
index 0000000..55724b6
--- /dev/null
+++ b/ticket.php
@@ -0,0 +1,107 @@
+ 0 ? qh_fetch_ticket($ticketId) : null;
+
+qh_page_start('home', 'Ticket detail', 'Detailed patient ticket timeline for the hospital queue workflow.');
+?>
+
+
+
+
+
+
+ Ticket not found.
+ Return to reception and choose a valid ticket.
+
+
+
+
+
+
+
= qh_h($ticket['ticket_number']) ?>
+
= qh_h($ticket['patient_name']) ?>
+
= qh_h($ticket['clinic_name_en'] ?? '') ?> · = qh_h($ticket['doctor_name_en'] ?? '') ?> · Room = qh_h($ticket['doctor_room'] ?? '--') ?>
+
+
+ = qh_status_badge($ticket['status']) ?>
+ Preferred language: = strtoupper(qh_h($ticket['language_pref'])) ?>
+
+
+
+
+
+
+
+
Visit timeline / خط سير الزيارة
+
+
+
+
+
Ticket issued / تم إصدار التذكرة
+
= qh_format_datetime($ticket['created_at']) ?>
+
+
+
+
+
+
+
Nursing vitals / العلامات الحيوية
+
= qh_h($ticket['vitals_notes'] ?: 'Waiting for nursing input.') ?>
+
+
+
+
+
+
+
Ready for doctor / جاهز للطبيب
+
Assigned to = qh_h($ticket['doctor_name_en'] ?? 'Doctor') ?>, room = qh_h($ticket['doctor_room'] ?? '--') ?>
+
+
+
+
+
+
Called to room / تم النداء للغرفة
+
= qh_format_datetime($ticket['called_at']) ?>
+
+
+
+
+
+
Visit closed / إغلاق الزيارة
+
= $ticket['status'] === 'done' ? 'Completed successfully.' : ($ticket['status'] === 'no_show' ? 'Marked as no-show.' : 'Still active.') ?>
+
+
+
+
+
+
+
+
Details / التفاصيل
+
+
Clinic = qh_h($ticket['clinic_name_en'] ?? '—') ?>
+
Doctor = qh_h($ticket['doctor_name_en'] ?? '—') ?>
+
Room = qh_h($ticket['doctor_room'] ?? '--') ?>
+
Vitals note = qh_h($ticket['vitals_notes'] ?: 'Not captured yet.') ?>
+
Last note = qh_h($ticket['display_note'] ?: '—') ?>
+
+
+
+
+
+
+
+