716 lines
29 KiB
PHP
716 lines
29 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
|
session_start();
|
|
}
|
|
|
|
require_once __DIR__ . '/db/config.php';
|
|
|
|
function qh_boot(): void
|
|
{
|
|
static $booted = false;
|
|
if ($booted) {
|
|
return;
|
|
}
|
|
|
|
qh_ensure_schema();
|
|
qh_seed_demo_data();
|
|
$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);
|
|
}
|
|
|
|
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"><span class="label-en">%2$s</span><span class="label-sep"> / </span><span class="label-ar" lang="ar" dir="rtl">%3$s</span></%1$s>',
|
|
$tag,
|
|
qh_h($en),
|
|
qh_h($ar)
|
|
);
|
|
}
|
|
|
|
function qh_page_start(string $activePage, string $pageTitle, string $metaDescription = ''): void
|
|
{
|
|
$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 '<!doctype html>';
|
|
echo '<html lang="en">';
|
|
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) . '">';
|
|
}
|
|
echo ' <meta name="theme-color" content="#f3f4f6">';
|
|
echo ' <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">';
|
|
echo ' <link rel="stylesheet" href="assets/css/custom.css?v=' . $assetVersionCss . '">';
|
|
echo '</head>';
|
|
echo '<body class="' . qh_h($bodyClass) . '" data-page="' . qh_h($activePage) . '">';
|
|
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
|
|
{
|
|
$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' => '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 '<header class="border-bottom bg-white 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 flex-column gap-0" href="index.php">';
|
|
echo ' <span class="brand-mark">HQ</span>';
|
|
echo ' <span class="brand-text">' . qh_h(qh_project_name()) . '</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="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']) . '">' . $link['label'] . '</a></li>';
|
|
}
|
|
echo ' </ul>';
|
|
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="Close"></button>';
|
|
echo ' </div>';
|
|
echo ' </div>';
|
|
echo '</div>';
|
|
}
|
|
|
|
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 '<span class="badge text-bg-' . qh_h($meta['class']) . ' status-badge">' . qh_label($meta['en'], $meta['ar']) . '</span>';
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|