1465 lines
66 KiB
PHP
1465 lines
66 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||
session_start();
|
||
}
|
||
$publicPages = ["login.php", "logout.php", "install.php", "display.php", "ticket.php"];
|
||
$currentPage = basename((string) ($_SERVER["PHP_SELF"] ?? "index.php"));
|
||
$isInstalled = file_exists(__DIR__ . "/.installed") || file_exists(__DIR__ . "/install.lock");
|
||
if (!$isInstalled) {
|
||
try {
|
||
if (file_exists(__DIR__ . '/db/config.php')) {
|
||
require_once __DIR__ . '/db/config.php';
|
||
if (function_exists('db')) {
|
||
$pdo = db();
|
||
$stmt = @$pdo->query("SELECT COUNT(*) FROM users");
|
||
if ($stmt !== false && ((int)$stmt->fetchColumn()) > 0) {
|
||
$isInstalled = true;
|
||
@file_put_contents(__DIR__ . '/install.lock', date('Y-m-d H:i:s'));
|
||
}
|
||
}
|
||
}
|
||
} catch (\Throwable $e) {}
|
||
}
|
||
|
||
if (!$isInstalled && $currentPage !== "install.php") {
|
||
header("Location: install.php");
|
||
exit;
|
||
}
|
||
if (!in_array($currentPage, $publicPages, true)) {
|
||
if (empty($_SESSION["user_id"])) {
|
||
header("Location: login.php");
|
||
exit;
|
||
}
|
||
|
||
$role = $_SESSION["role"] ?? "admin";
|
||
$allowed = false;
|
||
if ($role === "admin") {
|
||
$allowed = true;
|
||
} elseif ($currentPage === "index.php") {
|
||
$allowed = true;
|
||
} elseif ($role === "reception" && $currentPage === "reception.php") {
|
||
$allowed = true;
|
||
} elseif ($role === "nursing" && $currentPage === "nursing.php") {
|
||
$allowed = true;
|
||
} elseif ($role === "doctor" && $currentPage === "doctor.php") {
|
||
$allowed = true;
|
||
}
|
||
|
||
if (!$allowed) {
|
||
header("Location: index.php");
|
||
exit;
|
||
}
|
||
}
|
||
|
||
|
||
require_once __DIR__ . '/db/config.php';
|
||
|
||
function qh_boot(): void
|
||
{
|
||
static $booted = false;
|
||
if ($booted) {
|
||
return;
|
||
}
|
||
|
||
qh_ensure_schema();
|
||
qh_seed_demo_data();
|
||
qh_seed_hospital_profile();
|
||
|
||
$profile = qh_fetch_hospital_profile();
|
||
$timezone = $profile['timezone'] ?? 'UTC';
|
||
if ($timezone) {
|
||
date_default_timezone_set($timezone);
|
||
try {
|
||
$offset = (new DateTime('now', new DateTimeZone($timezone)))->format('P');
|
||
db()->exec("SET time_zone = " . db()->quote($offset));
|
||
} catch (\Throwable $e) {}
|
||
}
|
||
|
||
$booted = true;
|
||
}
|
||
|
||
function qh_ensure_schema(): void
|
||
{
|
||
$sql = <<<SQL
|
||
CREATE TABLE IF NOT EXISTS hospital_queue_records (
|
||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||
item_type VARCHAR(20) NOT NULL,
|
||
code VARCHAR(20) DEFAULT NULL,
|
||
name_en VARCHAR(120) DEFAULT NULL,
|
||
name_ar VARCHAR(120) DEFAULT NULL,
|
||
clinic_id INT UNSIGNED DEFAULT NULL,
|
||
doctor_id INT UNSIGNED DEFAULT NULL,
|
||
room_number VARCHAR(20) DEFAULT NULL,
|
||
requires_vitals TINYINT(1) NOT NULL DEFAULT 0,
|
||
patient_name VARCHAR(120) DEFAULT NULL,
|
||
language_pref VARCHAR(5) DEFAULT 'en',
|
||
ticket_number VARCHAR(20) DEFAULT NULL,
|
||
status VARCHAR(30) DEFAULT 'draft',
|
||
vitals_notes TEXT DEFAULT NULL,
|
||
display_note VARCHAR(255) DEFAULT NULL,
|
||
sort_order INT NOT NULL DEFAULT 0,
|
||
called_at DATETIME DEFAULT NULL,
|
||
served_at DATETIME DEFAULT NULL,
|
||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
INDEX idx_item_type (item_type),
|
||
INDEX idx_status (status),
|
||
INDEX idx_clinic_id (clinic_id),
|
||
INDEX idx_doctor_id (doctor_id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||
SQL;
|
||
|
||
db()->exec($sql);
|
||
|
||
$profileSql = <<<SQL
|
||
CREATE TABLE IF NOT EXISTS hospital_profile_settings (
|
||
id TINYINT UNSIGNED NOT NULL PRIMARY KEY,
|
||
name_en VARCHAR(160) DEFAULT NULL,
|
||
name_ar VARCHAR(160) DEFAULT NULL,
|
||
short_name VARCHAR(40) DEFAULT NULL,
|
||
tagline_en VARCHAR(255) DEFAULT NULL,
|
||
tagline_ar VARCHAR(255) DEFAULT NULL,
|
||
phone VARCHAR(60) DEFAULT NULL,
|
||
email VARCHAR(160) DEFAULT NULL,
|
||
website VARCHAR(255) DEFAULT NULL,
|
||
address_en VARCHAR(255) DEFAULT NULL,
|
||
address_ar VARCHAR(255) DEFAULT NULL,
|
||
working_hours_en VARCHAR(255) DEFAULT NULL,
|
||
working_hours_ar VARCHAR(255) DEFAULT NULL,
|
||
logo_url VARCHAR(255) DEFAULT NULL,
|
||
favicon_url VARCHAR(255) DEFAULT NULL,
|
||
primary_color VARCHAR(7) DEFAULT NULL,
|
||
secondary_color VARCHAR(7) DEFAULT NULL,
|
||
news_ticker_en VARCHAR(1000) DEFAULT NULL,
|
||
news_ticker_ar VARCHAR(1000) DEFAULT NULL,
|
||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||
SQL;
|
||
|
||
db()->exec($profileSql);
|
||
try { db()->exec("ALTER TABLE hospital_profile_settings ADD COLUMN news_ticker_en VARCHAR(1000) DEFAULT NULL"); } catch (\Throwable $e) {}
|
||
try { db()->exec("ALTER TABLE hospital_profile_settings ADD COLUMN news_ticker_ar VARCHAR(1000) DEFAULT NULL"); } catch (\Throwable $e) {}
|
||
try { db()->exec("ALTER TABLE hospital_profile_settings ADD COLUMN timezone VARCHAR(100) DEFAULT 'UTC'"); } catch (\Throwable $e) {}
|
||
try { db()->exec("ALTER TABLE hospital_profile_settings ADD COLUMN default_language VARCHAR(10) DEFAULT 'en'"); } catch (\Throwable $e) {}
|
||
}
|
||
|
||
try { db()->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"); } catch (\Throwable $e) {}
|
||
try { db()->exec("ALTER TABLE users ADD COLUMN role VARCHAR(20) DEFAULT 'admin'"); } catch (\Throwable $e) {}
|
||
|
||
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_seed_hospital_profile(): void
|
||
{
|
||
$exists = (int) db()->query('SELECT COUNT(*) FROM hospital_profile_settings')->fetchColumn();
|
||
if ($exists > 0) {
|
||
return;
|
||
}
|
||
|
||
$stmt = db()->prepare(
|
||
'INSERT INTO hospital_profile_settings
|
||
(id, name_en, name_ar, short_name, tagline_en, tagline_ar, phone, email, website, address_en, address_ar, working_hours_en, working_hours_ar, logo_url, favicon_url, primary_color, secondary_color, news_ticker_en, news_ticker_ar)
|
||
VALUES
|
||
(1, :name_en, :name_ar, :short_name, :tagline_en, :tagline_ar, :phone, :email, :website, :address_en, :address_ar, :working_hours_en, :working_hours_ar, :logo_url, :favicon_url, :primary_color, :secondary_color, :news_ticker_en, :news_ticker_ar)'
|
||
);
|
||
|
||
$stmt->execute([
|
||
'name_en' => qh_project_name('Hospital Queue Center'),
|
||
'name_ar' => 'مركز إدارة الطوابير',
|
||
'short_name' => 'HQC',
|
||
'tagline_en' => 'Organized patient flow, queue control, and staff coordination.',
|
||
'tagline_ar' => 'تنظيم تدفق المرضى وإدارة الطوابير وتنسيق عمل الطاقم.',
|
||
'phone' => '',
|
||
'email' => '',
|
||
'website' => '',
|
||
'address_en' => '',
|
||
'address_ar' => '',
|
||
'working_hours_en' => 'Sun–Thu · 8:00 AM – 8:00 PM',
|
||
'working_hours_ar' => 'الأحد – الخميس · 8:00 ص – 8:00 م',
|
||
'logo_url' => '',
|
||
'favicon_url' => '',
|
||
'primary_color' => '#0f8b8d',
|
||
'secondary_color' => '#16697a',
|
||
'news_ticker_en' => 'Welcome to our hospital.',
|
||
'news_ticker_ar' => 'مرحباً بكم في مستشفانا.',
|
||
]);
|
||
}
|
||
|
||
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_locale(): string
|
||
{
|
||
static $locale = null;
|
||
if ($locale !== null) {
|
||
return $locale;
|
||
}
|
||
|
||
$requested = strtolower(trim((string) ($_GET['lang'] ?? '')));
|
||
if (in_array($requested, ['en', 'ar'], true)) {
|
||
$_SESSION['qh_lang'] = $requested;
|
||
}
|
||
|
||
$sessionLocale = strtolower(trim((string) ($_SESSION['qh_lang'] ?? '')));
|
||
if (in_array($sessionLocale, ['en', 'ar'], true)) {
|
||
$locale = $sessionLocale;
|
||
} else {
|
||
$profile = qh_fetch_hospital_profile();
|
||
$defaultLang = strtolower(trim((string) ($profile['default_language'] ?? 'en')));
|
||
$locale = in_array($defaultLang, ['en', 'ar'], true) ? $defaultLang : 'en';
|
||
}
|
||
|
||
return $locale;
|
||
}
|
||
|
||
function qh_is_ar(): bool
|
||
{
|
||
return qh_locale() === 'ar';
|
||
}
|
||
|
||
function qh_t(string $en, string $ar): string
|
||
{
|
||
return qh_is_ar() ? $ar : $en;
|
||
}
|
||
|
||
function qh_locale_label(?string $locale = null): string
|
||
{
|
||
$locale = $locale ?: qh_locale();
|
||
return $locale === 'ar' ? 'العربية' : 'English';
|
||
}
|
||
|
||
function qh_other_locale(): string
|
||
{
|
||
return qh_is_ar() ? 'en' : 'ar';
|
||
}
|
||
|
||
function qh_url(string $path, array $params = []): string
|
||
{
|
||
$target = basename($path);
|
||
$params['lang'] = $params['lang'] ?? qh_locale();
|
||
$query = http_build_query($params);
|
||
return $query !== '' ? $target . '?' . $query : $target;
|
||
}
|
||
|
||
function qh_switch_lang_url(string $locale): string
|
||
{
|
||
$params = $_GET;
|
||
$params['lang'] = in_array($locale, ['en', 'ar'], true) ? $locale : qh_other_locale();
|
||
$target = basename((string) ($_SERVER['PHP_SELF'] ?? 'index.php'));
|
||
$query = http_build_query($params);
|
||
return $query !== '' ? $target . '?' . $query : $target;
|
||
}
|
||
|
||
function qh_name(array $row, string $base = 'name', string $fallback = '—'): string
|
||
{
|
||
$primaryKey = $base . '_' . (qh_is_ar() ? 'ar' : 'en');
|
||
$secondaryKey = $base . '_' . (qh_is_ar() ? 'en' : 'ar');
|
||
|
||
$primary = trim((string) ($row[$primaryKey] ?? ''));
|
||
if ($primary !== '') {
|
||
return $primary;
|
||
}
|
||
|
||
$secondary = trim((string) ($row[$secondaryKey] ?? ''));
|
||
return $secondary !== '' ? $secondary : $fallback;
|
||
}
|
||
|
||
function qh_fetch_hospital_profile(): array
|
||
{
|
||
static $profile = null;
|
||
if ($profile !== null) {
|
||
return $profile;
|
||
}
|
||
|
||
$row = db()->query('SELECT * FROM hospital_profile_settings WHERE id = 1 LIMIT 1')->fetch();
|
||
$profile = is_array($row) ? $row : [];
|
||
|
||
return $profile;
|
||
}
|
||
|
||
function qh_hospital_name(): string
|
||
{
|
||
$profile = qh_fetch_hospital_profile();
|
||
$name = qh_name($profile, 'name', '');
|
||
if ($name !== '') {
|
||
return $name;
|
||
}
|
||
|
||
$shortName = trim((string) ($profile['short_name'] ?? ''));
|
||
return $shortName !== '' ? $shortName : qh_project_name();
|
||
}
|
||
|
||
function qh_hospital_tagline(): string
|
||
{
|
||
$profile = qh_fetch_hospital_profile();
|
||
return qh_name($profile, 'tagline', '');
|
||
}
|
||
|
||
function qh_hospital_logo_url(): string
|
||
{
|
||
$profile = qh_fetch_hospital_profile();
|
||
return trim((string) ($profile['logo_url'] ?? ''));
|
||
}
|
||
|
||
function qh_hospital_favicon_url(): string
|
||
{
|
||
$profile = qh_fetch_hospital_profile();
|
||
$favicon = trim((string) ($profile['favicon_url'] ?? ''));
|
||
if ($favicon !== '') {
|
||
return $favicon;
|
||
}
|
||
|
||
return qh_hospital_logo_url();
|
||
}
|
||
|
||
function qh_hospital_contact_value(string $field): string
|
||
{
|
||
$profile = qh_fetch_hospital_profile();
|
||
return trim((string) ($profile[$field] ?? ''));
|
||
}
|
||
|
||
function qh_hospital_brand_initials(): string
|
||
{
|
||
$profile = qh_fetch_hospital_profile();
|
||
$source = trim((string) ($profile['short_name'] ?? ''));
|
||
if ($source === '') {
|
||
$source = trim((string) ($profile['name_en'] ?? qh_project_name('Hospital Queue')));
|
||
}
|
||
|
||
$parts = preg_split('/[^\p{L}\p{N}]+/u', $source, -1, PREG_SPLIT_NO_EMPTY);
|
||
$initials = [];
|
||
foreach ($parts as $part) {
|
||
if (preg_match('/^[\p{L}\p{N}]/u', $part, $matches)) {
|
||
$initials[] = strtoupper($matches[0]);
|
||
}
|
||
if (count($initials) >= 2) {
|
||
break;
|
||
}
|
||
}
|
||
|
||
return $initials !== [] ? implode('', array_slice($initials, 0, 2)) : 'HQ';
|
||
}
|
||
|
||
function qh_sanitize_hex_color(?string $value, string $fallback): string
|
||
{
|
||
$candidate = strtoupper(trim((string) $value));
|
||
return preg_match('/^#[0-9A-F]{6}$/', $candidate) ? $candidate : $fallback;
|
||
}
|
||
|
||
function qh_hospital_primary_color(): string
|
||
{
|
||
$profile = qh_fetch_hospital_profile();
|
||
return qh_sanitize_hex_color($profile['primary_color'] ?? null, '#0F8B8D');
|
||
}
|
||
|
||
function qh_hospital_secondary_color(): string
|
||
{
|
||
$profile = qh_fetch_hospital_profile();
|
||
return qh_sanitize_hex_color($profile['secondary_color'] ?? null, '#16697A');
|
||
}
|
||
|
||
function qh_current_language_badge(): string
|
||
{
|
||
return qh_t('English workspace', 'واجهة عربية');
|
||
}
|
||
|
||
function qh_admin_sections(): array
|
||
{
|
||
return [
|
||
'admin.php' => [
|
||
'label' => qh_t('Overview', 'نظرة عامة'),
|
||
'description' => qh_t('Admin home and setup summary.', 'الصفحة الرئيسية وملخص الإعدادات.'),
|
||
'icon' => 'overview',
|
||
],
|
||
'admin_ads.php' => [
|
||
'label' => qh_t('Advertisements', 'الإعلانات'),
|
||
'description' => qh_t('Manage videos shown on the queue display.', 'إدارة الفيديوهات المعروضة على شاشة الطابور.'),
|
||
'icon' => 'display',
|
||
],
|
||
'admin_hospital.php' => [
|
||
'label' => qh_t('Hospital profile', 'ملف المستشفى'),
|
||
'description' => qh_t('Manage logo, favicon, contact details, and brand colors.', 'إدارة الشعار والأيقونة وبيانات التواصل وألوان الهوية.'),
|
||
'icon' => 'hospital',
|
||
],
|
||
'admin_clinics.php' => [
|
||
'label' => qh_t('Clinics', 'العيادات'),
|
||
'description' => qh_t('Manage clinic codes, routing, and order.', 'إدارة رموز العيادات والمسار والترتيب.'),
|
||
'icon' => 'clinic',
|
||
],
|
||
'admin_users.php' => [
|
||
'label' => qh_t('Users', 'المستخدمون'),
|
||
'description' => qh_t('Manage system users and access.', 'إدارة مستخدمي النظام وصلاحيات الوصول.'),
|
||
'icon' => 'users',
|
||
],
|
||
'admin_doctors.php' => [
|
||
'label' => qh_t('Doctors', 'الأطباء'),
|
||
'description' => qh_t('Manage doctors, rooms, and assignments.', 'إدارة الأطباء والغرف والتعيينات.'),
|
||
'icon' => 'doctor',
|
||
],
|
||
];
|
||
}
|
||
|
||
function qh_admin_allowed_pages(): array
|
||
{
|
||
return array_keys(qh_admin_sections());
|
||
}
|
||
|
||
function qh_admin_return_to(?string $candidate = null): string
|
||
{
|
||
$page = basename((string) ($candidate ?? 'admin.php'));
|
||
return in_array($page, qh_admin_allowed_pages(), true) ? $page : 'admin.php';
|
||
}
|
||
|
||
function qh_admin_sidebar_icon(string $icon): string
|
||
{
|
||
return match ($icon) {
|
||
'hospital' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 20V7l7-3 7 3v13"/><path d="M9 11h1"/><path d="M14 11h1"/><path d="M11 8h2"/><path d="M11 20v-4h2v4"/><path d="M3 20h18"/></svg>',
|
||
'clinic' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20h16"/><path d="M6 20V8l6-4 6 4v12"/><path d="M9 12h1"/><path d="M14 12h1"/><path d="M11 20v-4h2v4"/></svg>',
|
||
'doctor' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/><circle cx="12" cy="12" r="8.5"/></svg>',
|
||
'users' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>',
|
||
default => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="4" width="6.5" height="6.5" rx="1.5"/><rect x="13.5" y="4" width="6.5" height="6.5" rx="1.5"/><rect x="4" y="13.5" width="6.5" height="6.5" rx="1.5"/><rect x="13.5" y="13.5" width="6.5" height="6.5" rx="1.5"/></svg>',
|
||
};
|
||
}
|
||
|
||
function qh_admin_stats(): array
|
||
{
|
||
$clinics = qh_fetch_clinics();
|
||
$doctors = qh_fetch_doctors();
|
||
$vitalsClinics = count(array_filter($clinics, static fn(array $clinic): bool => (int) ($clinic['requires_vitals'] ?? 0) === 1));
|
||
|
||
return [
|
||
'clinics' => count($clinics),
|
||
'doctors' => count($doctors),
|
||
'vitals_clinics' => $vitalsClinics,
|
||
'direct_clinics' => max(count($clinics) - $vitalsClinics, 0),
|
||
];
|
||
}
|
||
|
||
function qh_render_admin_sidebar(string $activePage, array $stats = []): void
|
||
{
|
||
$sections = qh_admin_sections();
|
||
$activePage = qh_admin_return_to($activePage);
|
||
if ($stats === []) {
|
||
$stats = qh_admin_stats();
|
||
}
|
||
|
||
echo '<div class="panel-card admin-sidebar">';
|
||
echo ' <div class="admin-sidebar-top">';
|
||
echo ' <span class="section-kicker">' . qh_h(qh_t('Admin panel', 'لوحة الإدارة')) . '</span>';
|
||
echo ' <h1 class="section-title mt-3 mb-1">' . qh_h(qh_hospital_name()) . '</h1>';
|
||
echo ' <p class="section-copy mb-0">' . qh_h(qh_hospital_tagline() !== '' ? qh_hospital_tagline() : qh_t('Move between dedicated pages instead of one long mixed admin screen.', 'تنقل بين صفحات مستقلة بدلاً من شاشة إدارة طويلة ومختلطة.')) . '</p>';
|
||
echo ' </div>';
|
||
echo ' <nav class="admin-sidebar-nav" aria-label="Admin section navigation">';
|
||
foreach ($sections as $file => $section) {
|
||
$activeClass = $file === $activePage ? ' active' : '';
|
||
$icon = qh_admin_sidebar_icon((string) ($section['icon'] ?? 'overview'));
|
||
echo ' <a class="admin-sidebar-link' . $activeClass . '" href="' . qh_h(qh_url($file)) . '" title="' . qh_h($section['description']) . '">';
|
||
echo ' <span class="admin-sidebar-link-icon">' . $icon . '</span>';
|
||
echo ' <span class="admin-sidebar-link-text">' . qh_h($section['label']) . '</span>';
|
||
echo ' </a>';
|
||
}
|
||
echo ' </nav>';
|
||
echo ' <div class="admin-sidebar-meta">';
|
||
echo ' <div class="admin-mini-stat"><span class="admin-mini-stat-value">' . qh_h((string) ($stats['clinics'] ?? 0)) . '</span><span class="admin-mini-stat-label">' . qh_h(qh_t('Clinics', 'العيادات')) . '</span></div>';
|
||
echo ' <div class="admin-mini-stat"><span class="admin-mini-stat-value">' . qh_h((string) ($stats['doctors'] ?? 0)) . '</span><span class="admin-mini-stat-label">' . qh_h(qh_t('Doctors', 'الأطباء')) . '</span></div>';
|
||
echo ' <div class="admin-mini-stat"><span class="admin-mini-stat-value">' . qh_h((string) ($stats['vitals_clinics'] ?? 0)) . '</span><span class="admin-mini-stat-label">' . qh_h(qh_t('Vitals-first clinics', 'العيادات التي تبدأ بالعلامات')) . '</span></div>';
|
||
echo ' </div>';
|
||
echo '</div>';
|
||
}
|
||
|
||
function qh_ticket_next_stop(array $ticket): string
|
||
{
|
||
return (int) ($ticket['clinic_requires_vitals'] ?? 0) === 1
|
||
? qh_t('Nursing vitals', 'العلامات الحيوية في التمريض')
|
||
: qh_t('Doctor waiting area', 'منطقة انتظار الطبيب');
|
||
}
|
||
|
||
function qh_ticket_last_note(array $ticket): string
|
||
{
|
||
return match ((string) ($ticket['status'] ?? '')) {
|
||
'waiting_vitals' => qh_t('Proceed to nursing vitals first.', 'يرجى التوجه أولاً إلى العلامات الحيوية في التمريض.'),
|
||
'ready_for_doctor' => qh_t('Wait for the doctor call on the public display.', 'انتظر نداء الطبيب على الشاشة العامة.'),
|
||
'called' => qh_t('The patient has been called to the doctor room.', 'تم نداء المريض إلى غرفة الطبيب.'),
|
||
'in_progress' => qh_t('The consultation is currently in progress.', 'الاستشارة جارية حالياً.'),
|
||
'done' => qh_t('The visit has been completed.', 'تم إكمال الزيارة.'),
|
||
'no_show' => qh_t('The patient was marked as no-show.', 'تم تسجيل المريض كحالة عدم حضور.'),
|
||
default => trim((string) ($ticket['display_note'] ?? '')) !== '' ? (string) $ticket['display_note'] : '—',
|
||
};
|
||
}
|
||
|
||
function qh_label(string $en, string $ar, string $wrapper = 'span'): string
|
||
{
|
||
$tag = preg_replace('/[^a-z0-9]/i', '', $wrapper) ?: 'span';
|
||
return sprintf('<%1$s>%2$s</%1$s>', $tag, qh_h(qh_t($en, $ar)));
|
||
}
|
||
|
||
|
||
|
||
function qh_page_start(string $activePage, string $pageTitle, string $metaDescription = ''): void
|
||
{
|
||
$locale = qh_locale();
|
||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? qh_project_description();
|
||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||
$brandName = qh_hospital_name();
|
||
$faviconUrl = qh_hospital_favicon_url();
|
||
$fullTitle = $pageTitle . ' · ' . $brandName;
|
||
$description = $metaDescription !== '' ? $metaDescription : qh_project_description();
|
||
$bodyClass = 'page-' . preg_replace('/[^a-z0-9\-]/i', '-', $activePage) . ' lang-' . $locale;
|
||
$assetVersionCss = qh_asset_version('assets/css/custom.css');
|
||
$primaryColor = qh_hospital_primary_color();
|
||
$secondaryColor = qh_hospital_secondary_color();
|
||
|
||
echo '<!doctype html>';
|
||
echo '<html lang="' . qh_h($locale) . '" dir="' . (qh_is_ar() ? 'rtl' : 'ltr') . '">';
|
||
echo '<head>';
|
||
echo ' <meta charset="utf-8">';
|
||
echo ' <meta name="viewport" content="width=device-width, initial-scale=1">';
|
||
echo ' <title>' . qh_h($fullTitle) . '</title>';
|
||
echo ' <meta name="description" content="' . qh_h($description) . '">';
|
||
if ($projectDescription) {
|
||
echo ' <meta property="og:description" content="' . qh_h((string) $projectDescription) . '">';
|
||
echo ' <meta property="twitter:description" content="' . qh_h((string) $projectDescription) . '">';
|
||
}
|
||
if ($projectImageUrl) {
|
||
echo ' <meta property="og:image" content="' . qh_h((string) $projectImageUrl) . '">';
|
||
echo ' <meta property="twitter:image" content="' . qh_h((string) $projectImageUrl) . '">';
|
||
}
|
||
if ($faviconUrl !== '') {
|
||
echo ' <link rel="icon" href="' . qh_h($faviconUrl) . '">';
|
||
echo ' <link rel="apple-touch-icon" href="' . qh_h($faviconUrl) . '">';
|
||
}
|
||
echo ' <meta name="theme-color" content="' . qh_h($primaryColor) . '">';
|
||
echo ' <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">';
|
||
echo ' <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">';
|
||
echo ' <link rel="stylesheet" href="assets/css/custom.css?v=' . $assetVersionCss . '">';
|
||
echo ' <style>:root{--accent:' . qh_h($primaryColor) . ';--accent-strong:' . qh_h($secondaryColor) . ';}</style>';
|
||
echo '</head>';
|
||
$profile = qh_fetch_hospital_profile();
|
||
$timezone = qh_h($profile['timezone'] ?? 'UTC');
|
||
echo '<body class="' . qh_h($bodyClass) . '" data-page="' . qh_h($activePage) . '" data-locale="' . qh_h($locale) . '" data-timezone="' . $timezone . '">';
|
||
if ($activePage !== 'display') {
|
||
qh_render_nav($activePage);
|
||
}
|
||
echo '<main class="app-shell py-4 py-lg-5">';
|
||
qh_render_flash();
|
||
}
|
||
|
||
|
||
|
||
function qh_page_end(): void
|
||
{
|
||
$profile = qh_fetch_hospital_profile();
|
||
$newsTicker = trim((string)($profile['news_ticker'] ?? ''));
|
||
|
||
if ($newsTicker !== '') {
|
||
echo '<div style="position: fixed; bottom: 0; left: 0; width: 100%; height: 40px; line-height: 40px; background: var(--accent-strong, #16697A); color: #fff; z-index: 9999; font-size: 1.25rem; font-weight: bold; overflow: hidden; box-shadow: 0 -2px 10px rgba(0,0,0,0.1);">';
|
||
echo '<marquee direction="right" scrollamount="6" scrolldelay="85">' . qh_h($newsTicker) . '</marquee>';
|
||
echo '</div>';
|
||
}
|
||
|
||
$assetVersionJs = qh_asset_version('assets/js/main.js');
|
||
echo '</main>';
|
||
echo '<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>';
|
||
echo '<script src="assets/js/main.js?v=' . $assetVersionJs . '"></script>';
|
||
echo '</body></html>';
|
||
}
|
||
|
||
function qh_render_nav(string $activePage): void
|
||
{
|
||
$links = [
|
||
'home' => ['href' => qh_url('index.php'), 'label' => qh_t('Operations', 'العمليات')],
|
||
'admin' => ['href' => qh_url('admin.php'), 'label' => qh_t('Admin', 'الإدارة')],
|
||
'reception' => ['href' => qh_url('reception.php'), 'label' => qh_t('Reception', 'الاستقبال')],
|
||
'nursing' => ['href' => qh_url('nursing.php'), 'label' => qh_t('Nursing', 'التمريض')],
|
||
'doctor' => ['href' => qh_url('doctor.php'), 'label' => qh_t('Doctor', 'الطبيب')],
|
||
'users' => ['href' => qh_url('admin_users.php'), 'label' => qh_t('Users', 'المستخدمون')],
|
||
'display' => ['href' => qh_url('display.php'), 'label' => qh_t('Display', 'الشاشة')],
|
||
];
|
||
|
||
$logoUrl = qh_hospital_logo_url();
|
||
$tagline = qh_hospital_tagline();
|
||
|
||
echo '<header class="app-header border-bottom sticky-top shadow-sm">';
|
||
echo ' <nav class="navbar navbar-expand-lg">';
|
||
echo ' <div class="container-fluid container-xxl px-3 px-lg-4">';
|
||
echo ' <a class="navbar-brand d-flex align-items-center" href="' . qh_h(qh_url('index.php')) . '">';
|
||
if ($logoUrl !== '') {
|
||
echo ' <span class="brand-mark brand-mark-image"><img src="' . qh_h($logoUrl) . '" alt="' . qh_h(qh_hospital_name()) . '"></span>';
|
||
} else {
|
||
echo ' <span class="brand-mark">' . qh_h(qh_hospital_brand_initials()) . '</span>';
|
||
}
|
||
echo ' <span class="brand-copy">';
|
||
echo ' <span class="brand-text">' . qh_h(qh_hospital_name()) . '</span>';
|
||
if ($tagline !== '') {
|
||
echo ' <span class="brand-subtext">' . qh_h($tagline) . '</span>';
|
||
}
|
||
echo ' </span>';
|
||
echo ' </a>';
|
||
echo ' <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#appNav" aria-controls="appNav" aria-expanded="false" aria-label="' . qh_h(qh_t('Toggle navigation', 'تبديل التنقل')) . '">';
|
||
echo ' <span class="navbar-toggler-icon"></span>';
|
||
echo ' </button>';
|
||
echo ' <div class="collapse navbar-collapse" id="appNav">';
|
||
echo ' <ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2 mt-3 mt-lg-0">';
|
||
foreach ($links as $key => $link) {
|
||
$activeClass = $key === $activePage ? ' active' : '';
|
||
echo ' <li class="nav-item"><a class="nav-link' . $activeClass . '" href="' . qh_h($link['href']) . '">' . qh_h($link['label']) . '</a></li>';
|
||
}
|
||
if (!empty($_SESSION["user_id"])) {
|
||
echo ' <li class="nav-item"><a class="nav-link text-danger fw-semibold" href="' . qh_h(qh_url("logout.php")) . '"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="me-1"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>' . qh_h(qh_t("Logout", "تسجيل الخروج")) . '</a></li>';
|
||
}
|
||
|
||
echo ' </ul>';
|
||
echo ' <div class="lang-switcher ms-lg-3 mt-3 mt-lg-0">';
|
||
foreach (['en', 'ar'] as $lang) {
|
||
$activeClass = qh_locale() === $lang ? ' active' : '';
|
||
echo ' <a class="lang-switch-link' . $activeClass . '" href="' . qh_h(qh_switch_lang_url($lang)) . '">' . qh_h(qh_locale_label($lang)) . '</a>';
|
||
}
|
||
echo ' </div>';
|
||
echo ' </div>';
|
||
echo ' </div>';
|
||
echo ' </nav>';
|
||
echo '</header>';
|
||
}
|
||
|
||
|
||
|
||
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 '<div class="toast-container position-fixed top-0 end-0 p-3">';
|
||
echo ' <div class="toast app-toast js-app-toast text-bg-' . qh_h($toastType) . ' border-0" role="status" aria-live="polite" aria-atomic="true">';
|
||
echo ' <div class="d-flex">';
|
||
echo ' <div class="toast-body">' . qh_h((string) $flash['message']) . '</div>';
|
||
echo ' <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="' . qh_h(qh_t('Close', 'إغلاق')) . '"></button>';
|
||
echo ' </div>';
|
||
echo ' </div>';
|
||
echo '</div>';
|
||
}
|
||
|
||
|
||
|
||
function qh_redirect(string $location): void
|
||
{
|
||
if (strpos($location, 'lang=') === false) {
|
||
$separator = str_contains($location, '?') ? '&' : '?';
|
||
$location .= $separator . 'lang=' . qh_locale();
|
||
}
|
||
|
||
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' AND DATE(t.created_at) = CURDATE()";
|
||
|
||
$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 IN ('waiting_vitals', 'nursing_called') 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 DATE(created_at) = CURDATE() AND status IN ('waiting_vitals', 'nursing_called')")->fetchColumn(),
|
||
'ready_for_doctor' => (int) $pdo->query("SELECT COUNT(*) FROM hospital_queue_records WHERE item_type = 'ticket' AND DATE(created_at) = CURDATE() 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 DATE(created_at) = CURDATE() 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(qh_t('Clinic not found.', 'لم يتم العثور على العيادة.'));
|
||
}
|
||
|
||
$doctor = qh_fetch_doctor($doctorId);
|
||
if (!$doctor || (int) $doctor['clinic_id'] !== $clinicId) {
|
||
throw new RuntimeException(qh_t('The 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 DATE(created_at) = CURDATE()
|
||
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' => 'بانتظار العلامات الحيوية'],
|
||
'nursing_called' => ['class' => 'primary', 'en' => 'Nursing Call', '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 '<span class="badge text-bg-' . qh_h($meta['class']) . ' status-badge">' . qh_h(qh_t($meta['en'], $meta['ar'])) . '</span>';
|
||
}
|
||
|
||
|
||
|
||
function qh_call_message(array $ticket): array
|
||
{
|
||
$ticketNumber = $ticket['ticket_number'] ?? '---';
|
||
// For English speech, replacing the hyphen with a space helps it say "DRR 001" instead of "DRR minus 001"
|
||
$speechEn = strtoupper(str_replace('-', ' ', $ticketNumber));
|
||
|
||
// Map English letters to Arabic phonetics so Arabic TTS pronounces them correctly
|
||
$arMap = [
|
||
'A'=>'إيه ', 'B'=>'بي ', 'C'=>'سي ', 'D'=>'دي ', 'E'=>'إي ', 'F'=>'إف ', 'G'=>'جي ', 'H'=>'إتش ',
|
||
'I'=>'آي ', 'J'=>'جيه ', 'K'=>'كيه ', 'L'=>'إل ', 'M'=>'إم ', 'N'=>'إن ', 'O'=>'أو ', 'P'=>'بي ',
|
||
'Q'=>'كيو ', 'R'=>'آر ', 'S'=>'إس ', 'T'=>'تي ', 'U'=>'يو ', 'V'=>'في ', 'W'=>'دبليو ', 'X'=>'إكس ',
|
||
'Y'=>'واي ', 'Z'=>'زِد '
|
||
];
|
||
$speechAr = trim(preg_replace('/ +/', ' ', strtr($speechEn, $arMap)));
|
||
|
||
if (($ticket['status'] ?? '') === 'nursing_called') {
|
||
return [
|
||
'en' => sprintf('Ticket %s, please proceed to Nursing Station.', $ticketNumber),
|
||
'ar' => sprintf('رقم التذكرة %s، يرجى التوجه إلى محطة التمريض.', $ticketNumber),
|
||
'speech_en' => sprintf('Ticket %s, please proceed to Nursing Station.', $speechEn),
|
||
'speech_ar' => sprintf('رقم التذكرة %s، يرجى التوجه إلى محطة التمريض.', $speechAr),
|
||
];
|
||
}
|
||
|
||
$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),
|
||
'speech_en' => sprintf('Ticket %s, please proceed to room %s for %s.', $speechEn, $room, $doctorNameEn),
|
||
'speech_ar' => sprintf('رقم التذكرة %s، يرجى التوجه إلى الغرفة %s إلى %s.', $speechAr, $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', qh_t('Invalid request method.', 'طريقة الطلب غير صالحة.'));
|
||
qh_redirect('index.php');
|
||
}
|
||
}
|
||
|
||
|
||
|
||
function qh_handle_image_upload(string $inputName): string
|
||
{
|
||
if (!isset($_FILES[$inputName]) || $_FILES[$inputName]['error'] !== UPLOAD_ERR_OK) {
|
||
return '';
|
||
}
|
||
|
||
$tmpName = $_FILES[$inputName]['tmp_name'];
|
||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||
if ($finfo === false) return '';
|
||
$mime = finfo_file($finfo, $tmpName);
|
||
finfo_close($finfo);
|
||
|
||
if (!is_string($mime) || !str_starts_with($mime, 'image/')) {
|
||
throw new InvalidArgumentException(qh_t('Invalid image format.', 'تنسيق صورة غير صالح.'));
|
||
}
|
||
|
||
$ext = match ($mime) {
|
||
'image/jpeg' => '.jpg',
|
||
'image/png' => '.png',
|
||
'image/gif' => '.gif',
|
||
'image/webp' => '.webp',
|
||
'image/svg+xml' => '.svg',
|
||
'image/x-icon' => '.ico',
|
||
'image/vnd.microsoft.icon' => '.ico',
|
||
default => '.bin'
|
||
};
|
||
|
||
if ($ext === '.bin') {
|
||
throw new InvalidArgumentException(qh_t('Unsupported image type.', 'نوع الصورة غير مدعوم.'));
|
||
}
|
||
|
||
$uploadDir = __DIR__ . '/assets/images/uploads';
|
||
if (!is_dir($uploadDir)) {
|
||
if (!mkdir($uploadDir, 0775, true)) {
|
||
throw new RuntimeException(qh_t('Failed to create upload directory.', 'فشل إنشاء مجلد الرفع.'));
|
||
}
|
||
}
|
||
|
||
$filename = uniqid('img_', true) . $ext;
|
||
$dest = $uploadDir . '/' . $filename;
|
||
|
||
if (!move_uploaded_file($tmpName, $dest)) {
|
||
throw new RuntimeException(qh_t('Failed to save uploaded file.', 'فشل حفظ الملف المرفوع.'));
|
||
}
|
||
|
||
return 'assets/images/uploads/' . $filename;
|
||
}
|
||
function qh_admin_handle_request(): void
|
||
{
|
||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||
return;
|
||
}
|
||
|
||
$action = trim((string) ($_POST['action'] ?? ''));
|
||
if ($action === '' || in_array($action, ['add_video', 'delete_video', 'toggle_status', 'add_news', 'delete_news', 'toggle_news_status', 'create_user', 'update_user', 'delete_user'])) {
|
||
return;
|
||
}
|
||
|
||
$returnTo = qh_admin_return_to($_POST['return_to'] ?? 'admin.php');
|
||
$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(qh_t('Please complete the clinic code and both clinic 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' => max((int) ($_POST['sort_order'] ?? 50), 1),
|
||
]);
|
||
qh_set_flash('success', qh_t('Clinic saved successfully.', 'تم حفظ العيادة بنجاح.'));
|
||
} elseif ($action === 'update_clinic') {
|
||
$clinicId = (int) ($_POST['clinic_id'] ?? 0);
|
||
$code = strtoupper(trim((string) ($_POST['code'] ?? '')));
|
||
$nameEn = trim((string) ($_POST['name_en'] ?? ''));
|
||
$nameAr = trim((string) ($_POST['name_ar'] ?? ''));
|
||
if ($clinicId <= 0 || $code === '' || $nameEn === '' || $nameAr === '') {
|
||
throw new InvalidArgumentException(qh_t('Please complete the clinic details before updating.', 'يرجى إكمال بيانات العيادة قبل التحديث.'));
|
||
}
|
||
$stmt = $pdo->prepare(
|
||
"UPDATE hospital_queue_records
|
||
SET code = :code,
|
||
name_en = :name_en,
|
||
name_ar = :name_ar,
|
||
requires_vitals = :requires_vitals,
|
||
sort_order = :sort_order
|
||
WHERE item_type = 'clinic' AND id = :clinic_id"
|
||
);
|
||
$stmt->execute([
|
||
'code' => substr($code, 0, 10),
|
||
'name_en' => $nameEn,
|
||
'name_ar' => $nameAr,
|
||
'requires_vitals' => isset($_POST['requires_vitals']) ? 1 : 0,
|
||
'sort_order' => max((int) ($_POST['sort_order'] ?? 50), 1),
|
||
'clinic_id' => $clinicId,
|
||
]);
|
||
qh_set_flash('success', qh_t('Clinic updated successfully.', 'تم تحديث العيادة بنجاح.'));
|
||
} elseif ($action === 'delete_clinic') {
|
||
$clinicId = (int) ($_POST['clinic_id'] ?? 0);
|
||
if ($clinicId <= 0) {
|
||
throw new InvalidArgumentException(qh_t('Invalid clinic selected.', 'تم اختيار عيادة غير صالحة.'));
|
||
}
|
||
$doctorCountStmt = $pdo->prepare("SELECT COUNT(*) FROM hospital_queue_records WHERE item_type = 'doctor' AND clinic_id = :clinic_id");
|
||
$doctorCountStmt->execute(['clinic_id' => $clinicId]);
|
||
$doctorCount = (int) $doctorCountStmt->fetchColumn();
|
||
$ticketCountStmt = $pdo->prepare("SELECT COUNT(*) FROM hospital_queue_records WHERE item_type = 'ticket' AND clinic_id = :clinic_id");
|
||
$ticketCountStmt->execute(['clinic_id' => $clinicId]);
|
||
$ticketCount = (int) $ticketCountStmt->fetchColumn();
|
||
if ($doctorCount > 0 || $ticketCount > 0) {
|
||
throw new InvalidArgumentException(qh_t('This clinic cannot be deleted because it is linked to doctors or patient tickets.', 'لا يمكن حذف هذه العيادة لأنها مرتبطة بأطباء أو تذاكر مرضى.'));
|
||
}
|
||
$stmt = $pdo->prepare("DELETE FROM hospital_queue_records WHERE item_type = 'clinic' AND id = :clinic_id");
|
||
$stmt->execute(['clinic_id' => $clinicId]);
|
||
qh_set_flash('success', qh_t('Clinic deleted successfully.', 'تم حذف العيادة بنجاح.'));
|
||
} elseif ($action === 'add_doctor') {
|
||
$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(qh_t('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' => max((int) ($_POST['sort_order'] ?? 50), 1),
|
||
]);
|
||
qh_set_flash('success', qh_t('Doctor profile saved.', 'تم حفظ ملف الطبيب.'));
|
||
} elseif ($action === 'update_doctor') {
|
||
$doctorId = (int) ($_POST['doctor_id'] ?? 0);
|
||
$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 ($doctorId <= 0 || $nameEn === '' || $nameAr === '' || $clinicId <= 0 || $roomNumber === '') {
|
||
throw new InvalidArgumentException(qh_t('Please complete the doctor details before updating.', 'يرجى إكمال بيانات الطبيب قبل التحديث.'));
|
||
}
|
||
$stmt = $pdo->prepare(
|
||
"UPDATE hospital_queue_records
|
||
SET name_en = :name_en,
|
||
name_ar = :name_ar,
|
||
clinic_id = :clinic_id,
|
||
room_number = :room_number,
|
||
sort_order = :sort_order
|
||
WHERE item_type = 'doctor' AND id = :doctor_id"
|
||
);
|
||
$stmt->execute([
|
||
'name_en' => $nameEn,
|
||
'name_ar' => $nameAr,
|
||
'clinic_id' => $clinicId,
|
||
'room_number' => $roomNumber,
|
||
'sort_order' => max((int) ($_POST['sort_order'] ?? 50), 1),
|
||
'doctor_id' => $doctorId,
|
||
]);
|
||
qh_set_flash('success', qh_t('Doctor assignment updated.', 'تم تحديث تعيين الطبيب.'));
|
||
} elseif ($action === 'delete_doctor') {
|
||
$doctorId = (int) ($_POST['doctor_id'] ?? 0);
|
||
if ($doctorId <= 0) {
|
||
throw new InvalidArgumentException(qh_t('Invalid doctor selected.', 'تم اختيار طبيب غير صالح.'));
|
||
}
|
||
$ticketCountStmt = $pdo->prepare("SELECT COUNT(*) FROM hospital_queue_records WHERE item_type = 'ticket' AND doctor_id = :doctor_id");
|
||
$ticketCountStmt->execute(['doctor_id' => $doctorId]);
|
||
$ticketCount = (int) $ticketCountStmt->fetchColumn();
|
||
if ($ticketCount > 0) {
|
||
throw new InvalidArgumentException(qh_t('This doctor cannot be deleted because patient tickets are linked to the profile.', 'لا يمكن حذف هذا الطبيب لأن هناك تذاكر مرضى مرتبطة بالملف.'));
|
||
}
|
||
$stmt = $pdo->prepare("DELETE FROM hospital_queue_records WHERE item_type = 'doctor' AND id = :doctor_id");
|
||
$stmt->execute(['doctor_id' => $doctorId]);
|
||
qh_set_flash('success', qh_t('Doctor deleted successfully.', 'تم حذف الطبيب بنجاح.'));
|
||
} elseif ($action === 'save_hospital_profile') {
|
||
$nameEn = trim((string) ($_POST['name_en'] ?? ''));
|
||
$nameAr = trim((string) ($_POST['name_ar'] ?? ''));
|
||
$shortName = trim((string) ($_POST['short_name'] ?? ''));
|
||
$taglineEn = trim((string) ($_POST['tagline_en'] ?? ''));
|
||
$taglineAr = trim((string) ($_POST['tagline_ar'] ?? ''));
|
||
$phone = trim((string) ($_POST['phone'] ?? ''));
|
||
$email = trim((string) ($_POST['email'] ?? ''));
|
||
$website = trim((string) ($_POST['website'] ?? ''));
|
||
$addressEn = trim((string) ($_POST['address_en'] ?? ''));
|
||
$addressAr = trim((string) ($_POST['address_ar'] ?? ''));
|
||
$workingHoursEn = trim((string) ($_POST['working_hours_en'] ?? ''));
|
||
$workingHoursAr = trim((string) ($_POST['working_hours_ar'] ?? ''));
|
||
$newsTickerEn = trim((string) ($_POST['news_ticker_en'] ?? ''));
|
||
$newsTickerAr = trim((string) ($_POST['news_ticker_ar'] ?? ''));
|
||
$timezone = trim((string) ($_POST['timezone'] ?? ''));
|
||
if ($timezone === '' || !in_array($timezone, timezone_identifiers_list(), true)) $timezone = 'UTC';
|
||
$defaultLanguage = trim((string) ($_POST['default_language'] ?? 'en'));
|
||
if (!in_array($defaultLanguage, ['en', 'ar'], true)) $defaultLanguage = 'en';
|
||
$profile = qh_fetch_hospital_profile();
|
||
$logoUrl = $profile['logo_url'] ?? '';
|
||
$faviconUrl = $profile['favicon_url'] ?? '';
|
||
|
||
if (!empty($_POST['remove_logo'])) {
|
||
$logoUrl = '';
|
||
} else {
|
||
$newLogo = qh_handle_image_upload('logo_upload');
|
||
if ($newLogo !== '') $logoUrl = $newLogo;
|
||
}
|
||
|
||
if (!empty($_POST['remove_favicon'])) {
|
||
$faviconUrl = '';
|
||
} else {
|
||
$newFavicon = qh_handle_image_upload('favicon_upload');
|
||
if ($newFavicon !== '') $faviconUrl = $newFavicon;
|
||
}
|
||
$primaryColor = qh_sanitize_hex_color($_POST['primary_color'] ?? '', '#0F8B8D');
|
||
$secondaryColor = qh_sanitize_hex_color($_POST['secondary_color'] ?? '', '#16697A');
|
||
|
||
if ($nameEn === '' || $nameAr === '') {
|
||
throw new InvalidArgumentException(qh_t('Please enter both the English and Arabic hospital names.', 'يرجى إدخال اسم المستشفى بالإنجليزية والعربية.'));
|
||
}
|
||
if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
|
||
throw new InvalidArgumentException(qh_t('Please enter a valid hospital email address.', 'يرجى إدخال بريد إلكتروني صالح للمستشفى.'));
|
||
}
|
||
if ($website !== '' && filter_var($website, FILTER_VALIDATE_URL) === false) {
|
||
throw new InvalidArgumentException(qh_t('Please enter a valid website URL.', 'يرجى إدخال رابط موقع صالح.'));
|
||
}
|
||
|
||
|
||
$stmt = $pdo->prepare(
|
||
"UPDATE hospital_profile_settings
|
||
SET name_en = :name_en,
|
||
name_ar = :name_ar,
|
||
short_name = :short_name,
|
||
tagline_en = :tagline_en,
|
||
tagline_ar = :tagline_ar,
|
||
phone = :phone,
|
||
email = :email,
|
||
website = :website,
|
||
address_en = :address_en,
|
||
address_ar = :address_ar,
|
||
working_hours_en = :working_hours_en,
|
||
working_hours_ar = :working_hours_ar,
|
||
logo_url = :logo_url,
|
||
favicon_url = :favicon_url,
|
||
primary_color = :primary_color,
|
||
secondary_color = :secondary_color,
|
||
news_ticker_en = :news_ticker_en,
|
||
news_ticker_ar = :news_ticker_ar,
|
||
timezone = :timezone,
|
||
default_language = :default_language
|
||
WHERE id = 1"
|
||
);
|
||
$stmt->execute([
|
||
'name_en' => $nameEn,
|
||
'name_ar' => $nameAr,
|
||
'short_name' => substr($shortName, 0, 40),
|
||
'tagline_en' => $taglineEn,
|
||
'tagline_ar' => $taglineAr,
|
||
'phone' => $phone,
|
||
'email' => $email,
|
||
'website' => $website,
|
||
'address_en' => $addressEn,
|
||
'address_ar' => $addressAr,
|
||
'working_hours_en' => $workingHoursEn,
|
||
'working_hours_ar' => $workingHoursAr,
|
||
'logo_url' => $logoUrl,
|
||
'favicon_url' => $faviconUrl,
|
||
'primary_color' => $primaryColor,
|
||
'secondary_color' => $secondaryColor,
|
||
'news_ticker_en' => $newsTickerEn,
|
||
'news_ticker_ar' => $newsTickerAr,
|
||
'timezone' => $timezone,
|
||
'default_language' => $defaultLanguage,
|
||
]);
|
||
qh_set_flash('success', qh_t('Hospital profile updated successfully.', 'تم تحديث ملف المستشفى بنجاح.'));
|
||
}
|
||
} catch (Throwable $exception) {
|
||
qh_set_flash('danger', $exception->getMessage());
|
||
}
|
||
|
||
qh_redirect($returnTo);
|
||
}
|
||
|
||
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(qh_t('Please complete the patient name, clinic, and doctor.', 'يرجى إدخال اسم المريض والعيادة والطبيب.'));
|
||
}
|
||
|
||
$ticketId = qh_create_ticket($patientName, $clinicId, $doctorId, $languagePref);
|
||
qh_set_flash('success', qh_t('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);
|
||
$action = trim((string) ($_POST['action'] ?? 'send'));
|
||
|
||
$ticket = qh_fetch_ticket($ticketId);
|
||
if (!$ticket) throw new InvalidArgumentException(qh_t('Invalid ticket.', 'تذكرة غير صالحة.'));
|
||
|
||
if ($action === 'call_ticket') {
|
||
$stmt = db()->prepare(
|
||
"UPDATE hospital_queue_records
|
||
SET status = 'nursing_called', called_at = NOW(), display_note = :display_note
|
||
WHERE item_type = 'ticket' AND id = :ticket_id"
|
||
);
|
||
$stmt->execute([
|
||
'display_note' => sprintf('Ticket %s, proceed to Nursing Station.', $ticket['ticket_number']),
|
||
'ticket_id' => $ticketId
|
||
]);
|
||
qh_set_flash('success', qh_t('Patient call was sent to the public display.', 'تم إرسال نداء المريض إلى الشاشة العامة.'));
|
||
} else {
|
||
$vitalsNotes = trim((string) ($_POST['vitals_notes'] ?? ''));
|
||
if ($vitalsNotes === '') throw new InvalidArgumentException(qh_t('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 IN ('waiting_vitals', 'nursing_called')"
|
||
);
|
||
$stmt->execute([
|
||
'vitals_notes' => $vitalsNotes,
|
||
'ticket_id' => $ticketId,
|
||
]);
|
||
qh_set_flash('success', qh_t('Vitals captured and patient moved to the 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(qh_t('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', qh_t('Patient call was sent 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', qh_t('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', qh_t('Visit marked as completed.', 'تم إنهاء الزيارة.'));
|
||
} elseif ($action === 'refer_ticket') {
|
||
$referToDoctorId = (int) ($_POST["refer_to_doctor_id"] ?? 0);
|
||
if ($referToDoctorId <= 0 || $referToDoctorId === $doctorId) {
|
||
throw new InvalidArgumentException(qh_t('Please select a valid doctor to refer the patient to.', 'يرجى اختيار طبيب صالح لتحويل المريض إليه.'));
|
||
}
|
||
$stmt = db()->prepare(
|
||
"UPDATE hospital_queue_records
|
||
SET status = 'ready_for_doctor', doctor_id = :refer_to_doctor_id, display_note = 'Referred'
|
||
WHERE item_type = 'ticket' AND id = :ticket_id"
|
||
);
|
||
$stmt->execute([
|
||
'refer_to_doctor_id' => $referToDoctorId,
|
||
'ticket_id' => $ticketId,
|
||
]);
|
||
qh_set_flash('success', qh_t('Patient referred successfully.', 'تم تحويل المريض بنجاح.'));
|
||
} 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', qh_t('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');
|
||
}
|
||
}
|