install file

This commit is contained in:
Flatlogic Bot 2026-04-01 03:29:17 +00:00
parent e2985ac197
commit a2f276f05e
7 changed files with 391 additions and 13 deletions

View File

@ -1309,5 +1309,5 @@ html[dir="rtl"] .news-ticker-item::after {
}
body {
padding-bottom: 3.5rem;
padding-bottom: 0 !important;
}

View File

@ -32,9 +32,16 @@ qh_page_start(
qh_t('Public queue display.', 'شاشة طوابير عامة.')
);
?>
<div class="container-fluid px-3 px-lg-4 py-3" data-auto-refresh="20">
<style>
/* Remove top spacing from the shell wrapper */
main.app-shell {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
</style>
<div class="container-fluid px-0 px-lg-0 py-0 m-0" data-auto-refresh="20" style="min-height: 100vh; display: flex; flex-direction: column; ">
<!-- Top Header for Display Board -->
<header class="d-flex justify-content-between align-items-center mb-4 bg-white p-3 rounded shadow-sm border-0">
<header class="d-flex justify-content-between align-items-center bg-white py-2 px-3 shadow-sm border-0">
<div class="d-flex align-items-center gap-3">
<?php if ($logoUrl = qh_hospital_logo_url()): ?>
<img src="<?= qh_h($logoUrl) ?>" alt="<?= qh_h(qh_hospital_name()) ?>" style="height: 50px;">
@ -56,10 +63,11 @@ qh_page_start(
</div>
</header>
<div class="row g-4 h-100">
<?php $pb = !empty($activeNews) ? "padding-bottom: 4rem !important;" : "padding-bottom: 1.5rem !important;"; ?>
<div class="row g-4 m-0 mt-2 px-3 px-lg-4 pt-0 flex-grow-1" style="<?= $pb ?>">
<div class="col-xl-8 col-lg-7 d-flex flex-column gap-4">
<div class="card shadow-sm border-0 flex-grow-1">
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
<div class="card-header bg-white border-bottom py-2 d-flex justify-content-between align-items-center">
<div>
<h2 class="h4 mb-0 text-gray-800 fw-bold"><?= qh_h(qh_t('Now Serving', 'يتم الآن النداء')) ?></h2>
</div>
@ -98,7 +106,7 @@ qh_page_start(
</div>
<div class="card shadow-sm border-0">
<div class="card-header bg-white border-bottom py-3">
<div class="card-header bg-white border-bottom py-2">
<h5 class="mb-0 font-weight-bold text-dark"><?= qh_h(qh_t('Queue by Clinic', 'الإنتظار حسب العيادة')) ?></h5>
</div>
<div class="card-body p-4 bg-light">
@ -171,7 +179,7 @@ qh_page_start(
</div>
<?php else: ?>
<div class="card shadow-sm border-0 h-100 bg-primary text-white">
<div class="card-header border-bottom border-light border-opacity-25 py-3 bg-transparent d-flex justify-content-between align-items-center">
<div class="card-header border-bottom border-light border-opacity-25 py-2 bg-transparent d-flex justify-content-between align-items-center">
<h5 class="mb-0 font-weight-bold text-white"><i class="bi bi-info-circle me-2"></i><?= qh_h(qh_t('Information', 'معلومات')) ?></h5>
<span class="badge bg-white text-primary rounded-pill small"><?= qh_h(qh_t('Notices', 'تنبيهات')) ?></span>
</div>

View File

@ -85,7 +85,7 @@ qh_page_start(
<td class="py-3 fw-semibold text-dark"><?= qh_h($ticket['patient_name']) ?></td>
<td class="py-3"><?= qh_status_badge($ticket['status']) ?></td>
<td class="text-end px-4 py-3">
<form method="post" class="d-inline-flex gap-2 flex-wrap justify-content-end">
<form method="post" class="d-inline-flex gap-2 flex-wrap justify-content-end align-items-center">
<input type="hidden" name="ticket_id" value="<?= qh_h((string) $ticket['id']) ?>"> <input type="hidden" name="doctor_id" value="<?= qh_h((string) $selectedDoctorId) ?>">
<?php if ($ticket['status'] === 'ready_for_doctor'): ?>
@ -97,6 +97,18 @@ qh_page_start(
<button class="btn btn-sm btn-outline-danger shadow-sm bg-white" type="submit" name="action" value="mark_no_show"><?= qh_h(qh_t('Not Show', 'غائب')) ?></button>
<button class="btn btn-sm btn-success shadow-sm" type="submit" name="action" value="complete_ticket"><?= qh_h(qh_t('Served', 'تمت الخدمة')) ?></button>
<div class="input-group input-group-sm ms-lg-2 mt-2 mt-lg-0" style="max-width: 180px;">
<select name="refer_to_doctor_id" class="form-select border-secondary text-secondary">
<option value=""><?= qh_h(qh_t('Refer to...', 'تحويل إلى...')) ?></option>
<?php foreach ($doctors as $d): if ($d['id'] == $selectedDoctorId) continue; ?>
<option value="<?= qh_h((string) $d['id']) ?>"><?= qh_h(qh_name($d)) ?></option>
<?php endforeach; ?>
</select>
<button class="btn btn-outline-secondary bg-white" type="submit" name="action" value="refer_ticket" title="<?= qh_h(qh_t('Refer Patient', 'تحويل المريض')) ?>">
<i class="bi bi-arrow-right-circle"></i>
</button>
</div>
</form>
</td>
</tr>

214
install.php Normal file
View File

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

99
login.php Normal file
View File

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

12
logout.php Normal file
View File

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

View File

@ -4,6 +4,19 @@ 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"));
if (!file_exists(__DIR__ . "/.installed") && $currentPage !== "install.php") {
header("Location: install.php");
exit;
}
if (file_exists(__DIR__ . "/.installed") && !in_array($currentPage, $publicPages, true)) {
if (empty($_SESSION["user_id"])) {
header("Location: login.php");
exit;
}
}
require_once __DIR__ . '/db/config.php';
@ -639,6 +652,10 @@ function qh_render_nav(string $activePage): void
$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) {
@ -754,7 +771,7 @@ function qh_fetch_tickets(array $statuses = [], ?int $doctorId = null, ?int $lim
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'";
WHERE t.item_type = 'ticket' AND DATE(t.created_at) = CURDATE()";
$params = [];
@ -808,9 +825,9 @@ 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(),
'waiting_vitals' => (int) $pdo->query("SELECT COUNT(*) FROM hospital_queue_records WHERE item_type = 'ticket' AND DATE(created_at) = CURDATE() AND status = 'waiting_vitals'")->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(),
];
}
@ -858,6 +875,7 @@ function qh_generate_ticket_number(string $clinicCode): string
"SELECT COUNT(*)
FROM hospital_queue_records
WHERE item_type = 'ticket'
AND DATE(created_at) = CURDATE()
AND ticket_number LIKE :prefix"
);
$stmt->execute(['prefix' => $prefix . '-%']);
@ -1326,6 +1344,21 @@ function qh_doctor_handle_request(): void
);
$stmt->execute(['ticket_id' => $ticketId]);
qh_set_flash('success', qh_t('Visit marked as completed.', 'تم إنهاء الزيارة.'));
} elseif ($action === 'refer_ticket') {
$referToDoctorId = (int) ( emote_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