Autosave: 20260317-024109
This commit is contained in:
parent
e4e7b4a246
commit
2575e8e91e
@ -1,14 +1,19 @@
|
|||||||
Plan:
|
Plan:
|
||||||
1. **Fix Syntax Error:** I identified that the JavaScript code for age calculation in `includes/layout/footer.php` contained Python-style comments (`#`), causing a syntax error.
|
1. **Fix 500 Error:** I identified and fixed a critical syntax error in `includes/actions.php` (a missing quote in the SQL query) that was crashing the application.
|
||||||
2. **Correct Comments:** I created and executed a Python script (`fix_footer_js.py` and `fix_footer_js_v2.py`) to replace these with valid JavaScript comments (`//`).
|
2. **Restore Token Logic:** I carefully re-implemented the token generation code in `includes/actions.php` to ensure it works correctly without breaking the visit recording process.
|
||||||
3. **Verify:** I verified the file content to ensure the syntax is now correct.
|
3. **Add Missing UI:** I added the **"Issue Token"** checkbox to the "Details" tab of the "Add Visit" modal in `includes/layout/footer.php`, which was missing from the previous attempt.
|
||||||
|
4. **Verify Integrity:** I confirmed that the `patient_queue` database table exists and that all new files (`queue.php`, `api/queue.php`, `queue_display.php`) have valid syntax.
|
||||||
|
|
||||||
Changed:
|
Changed:
|
||||||
* `includes/layout/footer.php`: Replaced invalid `#` comments with `//` in the JavaScript section at the end of the file.
|
* `includes/actions.php`: Fixed a syntax error and correctly implemented the token generation logic.
|
||||||
|
* `includes/layout/footer.php`: Added the "Issue Token" checkbox to the "Details" tab of the `recordVisitModal`.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
* The automatic age-to-DOB calculation (and vice-versa) should now work correctly when you type in the "Age" field.
|
* The application should now load correctly without the 500 error.
|
||||||
* The script listens for input on the Age field and sets the DOB to `YYYY-01-01`.
|
* To use the token system:
|
||||||
|
1. **Receptionist:** When adding a new visit, check the **"Issue Token"** box in the "Details" tab.
|
||||||
|
2. **Queue Management:** Go to **"Queue Management"** in the sidebar to manage the queue (Call, Finish).
|
||||||
|
3. **Display:** Click **"Open TV Display"** to open the public waiting list on a separate screen.
|
||||||
|
* Reminder: click Save in the editor to sync changes.
|
||||||
|
|
||||||
Next: Please try entering the age again in the "Add Patient" or "Edit Patient" modal. It should now automatically populate the Date of Birth field.
|
Next: Please refresh your browser and try adding a visit with the "Issue Token" checkbox checked. Let me know if everything works as expected!
|
||||||
Reminder: Click Save in the editor to sync changes.
|
|
||||||
179
api/queue.php
Normal file
179
api/queue.php
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
require_once __DIR__ . '/../includes/actions.php'; // For permissions if needed
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$action = $_GET['action'] ?? '';
|
||||||
|
$lang = $_GET['lang'] ?? 'en';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = db();
|
||||||
|
|
||||||
|
// --- ADD TOKEN ---
|
||||||
|
if ($action === 'add') {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
throw new Exception('Invalid request method');
|
||||||
|
}
|
||||||
|
|
||||||
|
$patient_id = $_POST['patient_id'] ?? null;
|
||||||
|
$department_id = $_POST['department_id'] ?? null;
|
||||||
|
$doctor_id = $_POST['doctor_id'] ?? null;
|
||||||
|
|
||||||
|
if (!$patient_id || !$department_id) {
|
||||||
|
throw new Exception('Patient and Department are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get next token number for this department today
|
||||||
|
$today = date('Y-m-d');
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT MAX(token_number)
|
||||||
|
FROM patient_queue
|
||||||
|
WHERE department_id = ?
|
||||||
|
AND DATE(created_at) = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$department_id, $today]);
|
||||||
|
$max_token = $stmt->fetchColumn();
|
||||||
|
$next_token = ($max_token) ? $max_token + 1 : 1;
|
||||||
|
|
||||||
|
// Insert
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO patient_queue (patient_id, department_id, doctor_id, token_number, status, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, 'waiting', NOW())
|
||||||
|
");
|
||||||
|
$stmt->execute([$patient_id, $department_id, $doctor_id ?: null, $next_token]);
|
||||||
|
$queue_id = $db->lastInsertId();
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Token generated', 'token_number' => $next_token, 'queue_id' => $queue_id]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- LIST QUEUE ---
|
||||||
|
if ($action === 'list') {
|
||||||
|
$dept_id = $_GET['department_id'] ?? null;
|
||||||
|
$doc_id = $_GET['doctor_id'] ?? null;
|
||||||
|
$status = $_GET['status'] ?? null; // Can be comma separated 'waiting,serving'
|
||||||
|
$today = date('Y-m-d');
|
||||||
|
|
||||||
|
$where = "WHERE DATE(q.created_at) = ?";
|
||||||
|
$params = [$today];
|
||||||
|
|
||||||
|
if ($dept_id) {
|
||||||
|
$where .= " AND q.department_id = ?";
|
||||||
|
$params[] = $dept_id;
|
||||||
|
}
|
||||||
|
if ($doc_id) {
|
||||||
|
$where .= " AND (q.doctor_id = ? OR q.doctor_id IS NULL)";
|
||||||
|
$params[] = $doc_id;
|
||||||
|
}
|
||||||
|
if ($status) {
|
||||||
|
$statuses = explode(',', $status);
|
||||||
|
$placeholders = implode(',', array_fill(0, count($statuses), '?'));
|
||||||
|
$where .= " AND q.status IN ($placeholders)";
|
||||||
|
$params = array_merge($params, $statuses);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT q.*,
|
||||||
|
p.name as patient_name,
|
||||||
|
d.name_$lang as doctor_name,
|
||||||
|
d.name_en as doctor_name_en,
|
||||||
|
d.name_ar as doctor_name_ar,
|
||||||
|
dept.name_$lang as department_name,
|
||||||
|
dept.name_en as department_name_en,
|
||||||
|
dept.name_ar as department_name_ar
|
||||||
|
FROM patient_queue q
|
||||||
|
JOIN patients p ON q.patient_id = p.id
|
||||||
|
JOIN departments dept ON q.department_id = dept.id
|
||||||
|
LEFT JOIN doctors d ON q.doctor_id = d.id
|
||||||
|
$where
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN q.status = 'serving' THEN 1 WHEN q.status = 'waiting' THEN 2 ELSE 3 END,
|
||||||
|
q.token_number ASC
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$queue = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'data' => $queue]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- UPDATE STATUS ---
|
||||||
|
if ($action === 'update_status') {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
throw new Exception('Invalid request method');
|
||||||
|
}
|
||||||
|
|
||||||
|
$queue_id = $_POST['queue_id'] ?? null;
|
||||||
|
$new_status = $_POST['status'] ?? null;
|
||||||
|
$doctor_id = $_POST['doctor_id'] ?? null; // If a doctor picks up a general department token
|
||||||
|
|
||||||
|
if (!$queue_id || !$new_status) {
|
||||||
|
throw new Exception('Queue ID and Status are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($new_status, ['waiting', 'serving', 'completed', 'cancelled'])) {
|
||||||
|
throw new Exception('Invalid status');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic: If setting to 'serving', update doctor_id if provided
|
||||||
|
$sql = "UPDATE patient_queue SET status = ?, updated_at = NOW()";
|
||||||
|
$params = [$new_status];
|
||||||
|
|
||||||
|
if ($new_status === 'serving' && $doctor_id) {
|
||||||
|
$sql .= ", doctor_id = ?";
|
||||||
|
$params[] = $doctor_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= " WHERE id = ?";
|
||||||
|
$params[] = $queue_id;
|
||||||
|
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Status updated']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SUMMARY ---
|
||||||
|
if ($action === 'summary') {
|
||||||
|
$today = date('Y-m-d');
|
||||||
|
$dept_id = $_GET['department_id'] ?? null;
|
||||||
|
|
||||||
|
$where = "WHERE DATE(q.created_at) = ?";
|
||||||
|
$params = [$today];
|
||||||
|
|
||||||
|
if ($dept_id) {
|
||||||
|
$where .= " AND q.department_id = ?";
|
||||||
|
$params[] = $dept_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
dept.name_$lang as department_name,
|
||||||
|
dept.id as department_id,
|
||||||
|
SUM(CASE WHEN q.status = 'waiting' THEN 1 ELSE 0 END) as waiting,
|
||||||
|
SUM(CASE WHEN q.status = 'serving' THEN 1 ELSE 0 END) as serving,
|
||||||
|
SUM(CASE WHEN q.status = 'completed' THEN 1 ELSE 0 END) as completed
|
||||||
|
FROM patient_queue q
|
||||||
|
JOIN departments dept ON q.department_id = dept.id
|
||||||
|
$where
|
||||||
|
GROUP BY dept.id
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$summary = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'data' => $summary]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception('Invalid action');
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
9
check_patients_table.php
Normal file
9
check_patients_table.php
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->query("DESCRIBE patients");
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
echo $row['Field'] . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
19
db/migrations/20260316_create_patient_queue.sql
Normal file
19
db/migrations/20260316_create_patient_queue.sql
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
-- Create patient_queue table
|
||||||
|
CREATE TABLE IF NOT EXISTS patient_queue (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
patient_id INT NOT NULL,
|
||||||
|
department_id INT NOT NULL,
|
||||||
|
doctor_id INT NULL,
|
||||||
|
visit_id INT NULL,
|
||||||
|
token_number INT NOT NULL,
|
||||||
|
status ENUM('waiting', 'serving', 'completed', 'cancelled') DEFAULT 'waiting',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (patient_id) REFERENCES patients(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (doctor_id) REFERENCES doctors(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (visit_id) REFERENCES visits(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index for faster searching of today's queue
|
||||||
|
CREATE INDEX idx_queue_date_dept ON patient_queue(created_at, department_id);
|
||||||
37
debug_queue.php
Normal file
37
debug_queue.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
echo "<h2>Patient Queue (Latest 5)</h2>";
|
||||||
|
$stmt = $pdo->query("SELECT * FROM patient_queue ORDER BY id DESC LIMIT 5");
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
if ($rows) {
|
||||||
|
echo "<table border='1'><tr>";
|
||||||
|
foreach (array_keys($rows[0]) as $k) echo "<th>$k</th>";
|
||||||
|
echo "</tr>";
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
echo "<tr>";
|
||||||
|
foreach ($row as $v) echo "<td>$v</td>";
|
||||||
|
echo "</tr>";
|
||||||
|
}
|
||||||
|
echo "</table>";
|
||||||
|
} else {
|
||||||
|
echo "No records in patient_queue.<br>";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "<h2>Visits (Latest 5)</h2>";
|
||||||
|
$stmt = $pdo->query("SELECT id, patient_id, doctor_id, created_at FROM visits ORDER BY id DESC LIMIT 5");
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
if ($rows) {
|
||||||
|
echo "<table border='1'><tr>";
|
||||||
|
foreach (array_keys($rows[0]) as $k) echo "<th>$k</th>";
|
||||||
|
echo "</tr>";
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
echo "<tr>";
|
||||||
|
foreach ($row as $v) echo "<td>$v</td>";
|
||||||
|
echo "</tr>";
|
||||||
|
}
|
||||||
|
echo "</table>";
|
||||||
|
} else {
|
||||||
|
echo "No records in visits.<br>";
|
||||||
|
}
|
||||||
@ -102,6 +102,7 @@ $site_favicon = !empty($site_settings['company_favicon']) ? $site_settings['comp
|
|||||||
<a href="patients.php" class="sidebar-link <?php echo $section === 'patients' ? 'active' : ''; ?>"><i class="bi bi-people me-2"></i> <?php echo __('patients'); ?></a>
|
<a href="patients.php" class="sidebar-link <?php echo $section === 'patients' ? 'active' : ''; ?>"><i class="bi bi-people me-2"></i> <?php echo __('patients'); ?></a>
|
||||||
<a href="visits.php" class="sidebar-link <?php echo $section === 'visits' ? 'active' : ''; ?>"><i class="bi bi-clipboard2-pulse me-2"></i> <?php echo __('visits'); ?></a>
|
<a href="visits.php" class="sidebar-link <?php echo $section === 'visits' ? 'active' : ''; ?>"><i class="bi bi-clipboard2-pulse me-2"></i> <?php echo __('visits'); ?></a>
|
||||||
<a href="appointments.php" class="sidebar-link <?php echo $section === "appointments" ? "active" : ""; ?>"><i class="bi bi-calendar-event me-2"></i> <?php echo __("appointments"); ?></a>
|
<a href="appointments.php" class="sidebar-link <?php echo $section === "appointments" ? "active" : ""; ?>"><i class="bi bi-calendar-event me-2"></i> <?php echo __("appointments"); ?></a>
|
||||||
|
<a href="queue.php" class="sidebar-link <?php echo $section === 'queue' ? 'active' : ''; ?>"><i class="bi bi-list-ol me-2"></i> <?php echo __('queue_management'); ?></a>
|
||||||
|
|
||||||
<a href="#labSubmenu" data-bs-toggle="collapse" class="sidebar-link <?php echo in_array($section, ['laboratory_tests', 'test_groups', 'laboratory_inquiries']) ? 'active' : ''; ?> d-flex justify-content-between align-items-center">
|
<a href="#labSubmenu" data-bs-toggle="collapse" class="sidebar-link <?php echo in_array($section, ['laboratory_tests', 'test_groups', 'laboratory_inquiries']) ? 'active' : ''; ?> d-flex justify-content-between align-items-center">
|
||||||
<span><i class="bi bi-prescription2 me-2"></i> <?php echo __('laboratory'); ?></span>
|
<span><i class="bi bi-prescription2 me-2"></i> <?php echo __('laboratory'); ?></span>
|
||||||
|
|||||||
@ -89,7 +89,7 @@ unset($inquiry);
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="text-center py-5 text-muted">
|
<td colspan="6" class="text-center py-5 text-muted">
|
||||||
<i class="bi bi-question-circle display-4 d-block mb-3"></i>
|
<i class="bi bi-question-circle display-4 d-block mb-3"></i>
|
||||||
No inquiries found.
|
<?php echo __('no_data_found') ?? 'No inquiries found.'; ?>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
@ -101,7 +101,7 @@ unset($inquiry);
|
|||||||
<?php if ($inquiry['visit_id']): ?>
|
<?php if ($inquiry['visit_id']): ?>
|
||||||
<div class="small text-muted">
|
<div class="small text-muted">
|
||||||
<i class="bi bi-calendar-check me-1"></i>
|
<i class="bi bi-calendar-check me-1"></i>
|
||||||
Linked to Visit #<?php echo $inquiry['visit_id']; ?> (<?php echo date('Y-m-d', strtotime($inquiry['visit_date'])); ?>)
|
<?php echo __('visit'); ?> #<?php echo $inquiry['visit_id']; ?> (<?php echo date('Y-m-d', strtotime($inquiry['visit_date'])); ?>)
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
@ -154,3 +154,251 @@ unset($inquiry);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Inquiry Modal -->
|
||||||
|
<div class="modal fade" id="editInquiryModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<form action="<?php echo $_SERVER['PHP_SELF']; ?>?section=laboratory_inquiries" method="POST" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="action" value="edit_inquiry">
|
||||||
|
<input type="hidden" name="id" id="edit_inquiry_id">
|
||||||
|
<input type="hidden" name="visit_id" id="edit_lab_visit_id">
|
||||||
|
<div class="modal-content border-0 shadow">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title fw-bold text-white"><?php echo __('edit_inquiry'); ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"><?php echo __('patient_name'); ?></label>
|
||||||
|
<input type="text" name="patient_name" id="edit_inquiry_patient_name" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label"><?php echo __('inquiry_date'); ?></label>
|
||||||
|
<input type="datetime-local" name="inquiry_date" id="edit_inquiry_date" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label"><?php echo __('status'); ?></label>
|
||||||
|
<select name="status" id="edit_inquiry_status" class="form-select">
|
||||||
|
<option value="Pending"><?php echo __('Pending'); ?></option>
|
||||||
|
<option value="Completed"><?php echo __('Completed'); ?></option>
|
||||||
|
<option value="Cancelled"><?php echo __('Cancelled'); ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="form-label fw-bold"><?php echo __('tests'); ?></label>
|
||||||
|
<div class="table-responsive mb-2">
|
||||||
|
<table class="table table-bordered table-sm" id="editInquiryTestsTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php echo __('test'); ?></th>
|
||||||
|
<th><?php echo __('result'); ?></th>
|
||||||
|
<th><?php echo __('attachment'); ?></th>
|
||||||
|
<th style="width: 50px;"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="addEditInquiryTestRow()">
|
||||||
|
<i class="bi bi-plus-lg"></i> <?php echo __('add_test'); ?>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<label class="form-label"><?php echo __('notes'); ?></label>
|
||||||
|
<textarea name="notes" id="edit_inquiry_notes" class="form-control" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer bg-light">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo __('cancel'); ?></button>
|
||||||
|
<button type="submit" class="btn btn-primary px-4"><?php echo __('save'); ?></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Inquiry Modal -->
|
||||||
|
<div class="modal fade" id="deleteInquiryModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<form action="<?php echo $_SERVER['PHP_SELF']; ?>?section=laboratory_inquiries" method="POST">
|
||||||
|
<input type="hidden" name="action" value="delete_inquiry">
|
||||||
|
<input type="hidden" name="id" id="delete_inquiry_id">
|
||||||
|
<div class="modal-content border-0 shadow">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title fw-bold text-white"><?php echo __('delete'); ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="mb-0"><?php echo __('confirm_delete'); ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer bg-light">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo __('cancel'); ?></button>
|
||||||
|
<button type="submit" class="btn btn-danger px-4"><?php echo __('delete'); ?></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const allTestsData = <?php echo json_encode($GLOBALS['all_tests'] ?? []); ?>;
|
||||||
|
|
||||||
|
function addEditInquiryTestRow(test = null) {
|
||||||
|
const tbody = document.querySelector('#editInquiryTestsTable tbody');
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
|
||||||
|
let options = '<option value=""><?php echo __('select'); ?>...</option>';
|
||||||
|
allTestsData.forEach(t => {
|
||||||
|
const selected = (test && test.test_id == t.id) ? 'selected' : '';
|
||||||
|
options += `<option value="${t.id}" ${selected}>${t.name} ($${t.price})</option>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
let resultVal = test && test.result ? test.result.replace(/"/g, '"') : '';
|
||||||
|
let attachmentHtml = '<input type="file" name="attachments[]" class="form-control form-control-sm">';
|
||||||
|
let existingAttachment = '';
|
||||||
|
if (test && test.attachment) {
|
||||||
|
existingAttachment = `<input type="hidden" name="existing_attachments[]" value="${test.attachment}">`;
|
||||||
|
attachmentHtml += `<div class="mt-1 small"><a href="${test.attachment}" target="_blank" class="text-info"><i class="bi bi-paperclip"></i> <?php echo __('view_image'); ?></a></div>`;
|
||||||
|
} else {
|
||||||
|
existingAttachment = `<input type="hidden" name="existing_attachments[]" value="">`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td><select name="test_ids[]" class="form-select form-select-sm select2-modal">${options}</select></td>
|
||||||
|
<td><input type="text" name="results[]" class="form-control form-control-sm" value="${resultVal}"></td>
|
||||||
|
<td>${attachmentHtml}${existingAttachment}</td>
|
||||||
|
<td><button type="button" class="btn btn-sm btn-danger py-0 px-2" onclick="this.closest('tr').remove()"><i class="bi bi-x"></i></button></td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
if (typeof jQuery !== 'undefined') {
|
||||||
|
$(tr).find('.select2-modal').select2({ dropdownParent: $(tr).closest('.modal'), width: '100%' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEditInquiryModal(inquiry) {
|
||||||
|
if (!inquiry) return;
|
||||||
|
var el = document.getElementById('editInquiryModal');
|
||||||
|
if (el) {
|
||||||
|
document.getElementById('edit_inquiry_id').value = inquiry.id || '';
|
||||||
|
document.getElementById('edit_lab_visit_id').value = inquiry.visit_id || '';
|
||||||
|
document.getElementById('edit_inquiry_patient_name').value = inquiry.patient_name || inquiry.official_patient_name || '';
|
||||||
|
|
||||||
|
var fields = {
|
||||||
|
'edit_inquiry_status': inquiry.status || 'Pending',
|
||||||
|
'edit_inquiry_notes': inquiry.notes || ''
|
||||||
|
};
|
||||||
|
for (var id in fields) {
|
||||||
|
var field = document.getElementById(id);
|
||||||
|
if (field) field.value = fields[id];
|
||||||
|
}
|
||||||
|
if (inquiry.inquiry_date) {
|
||||||
|
var dateField = document.getElementById('edit_inquiry_date');
|
||||||
|
if (dateField) dateField.value = inquiry.inquiry_date.replace(' ', 'T').substring(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tbody = document.querySelector('#editInquiryTestsTable tbody');
|
||||||
|
if (tbody) tbody.innerHTML = '';
|
||||||
|
|
||||||
|
if (inquiry.tests && inquiry.tests.length > 0) {
|
||||||
|
inquiry.tests.forEach(test => {
|
||||||
|
addEditInquiryTestRow(test);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
addEditInquiryTestRow();
|
||||||
|
}
|
||||||
|
|
||||||
|
var modal = bootstrap.Modal.getOrCreateInstance(el);
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDeleteInquiryModal(id) {
|
||||||
|
var el = document.getElementById('deleteInquiryModal');
|
||||||
|
if (el) {
|
||||||
|
var field = document.getElementById('delete_inquiry_id');
|
||||||
|
if (field) field.value = id || '';
|
||||||
|
var modal = bootstrap.Modal.getOrCreateInstance(el);
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printInquiry(inquiry) {
|
||||||
|
if (!inquiry) return;
|
||||||
|
try {
|
||||||
|
const printWindow = window.open('', '_blank');
|
||||||
|
if (!printWindow) return;
|
||||||
|
const date = inquiry.inquiry_date ? new Date(inquiry.inquiry_date).toLocaleString() : '';
|
||||||
|
|
||||||
|
let testsHtml = '';
|
||||||
|
if (inquiry.tests && inquiry.tests.length > 0) {
|
||||||
|
inquiry.tests.forEach(t => {
|
||||||
|
testsHtml += `
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; border-bottom: 1px solid #eee;">${t.test_name}</td>
|
||||||
|
<td style="padding: 10px; border-bottom: 1px solid #eee; text-align: center;"><strong>${t.result || '-'}</strong></td>
|
||||||
|
<td style="padding: 10px; border-bottom: 1px solid #eee; text-align: center;">${t.reference_range || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
printWindow.document.write(`
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title><?php echo __('laboratory_inquiries'); ?> - ${inquiry.patient_name || inquiry.official_patient_name}</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; color: #333; line-height: 1.6; }
|
||||||
|
.header { text-align: center; border-bottom: 2px solid #002D62; padding-bottom: 20px; margin-bottom: 30px; }
|
||||||
|
.header h1 { margin: 0; color: #002D62; }
|
||||||
|
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 30px; }
|
||||||
|
.info-item b { color: #555; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }
|
||||||
|
th { background-color: #f8f9fa; text-align: left; padding: 12px; border-bottom: 2px solid #dee2e6; }
|
||||||
|
.footer { margin-top: 50px; border-top: 1px solid #eee; padding-top: 20px; text-align: center; font-size: 0.9em; color: #777; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>LABORATORY REPORT</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item"><b><?php echo __('patient'); ?>:</b> ${inquiry.patient_name || inquiry.official_patient_name}</div>
|
||||||
|
<div class="info-item"><b><?php echo __('date'); ?>:</b> ${date}</div>
|
||||||
|
<div class="info-item"><b><?php echo __('inquiry'); ?> #:</b> #${inquiry.id}</div>
|
||||||
|
<div class="info-item"><b><?php echo __('status'); ?>:</b> ${inquiry.status}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php echo __('test'); ?></th>
|
||||||
|
<th style="text-align: center;"><?php echo __('result'); ?></th>
|
||||||
|
<th style="text-align: center;"><?php echo __('normal_range'); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${testsHtml}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
${inquiry.notes ? `<div style="margin-top: 20px;"><b><?php echo __('notes'); ?>:</b><p>${inquiry.notes}</p></div>` : ''}
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p><?php echo __('computer_generated_report'); ?></p>
|
||||||
|
<p><?php echo __('printed_on'); ?>: ${new Date().toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.onload = function() { window.print(); setTimeout(function() { window.close(); }, 500); }
|
||||||
|
<\/script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
printWindow.document.close();
|
||||||
|
} catch (e) { console.error('Print inquiry failed:', e); }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
256
includes/pages/queue_management.php
Normal file
256
includes/pages/queue_management.php
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
<?php
|
||||||
|
// includes/pages/queue_management.php
|
||||||
|
|
||||||
|
// Fetch Departments
|
||||||
|
$departments = $db->query("SELECT * FROM departments")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
// Fetch Doctors
|
||||||
|
$doctors = $db->query("SELECT * FROM doctors")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
// Fetch Patients (Limit 50 for initial load, preferably use AJAX for real search)
|
||||||
|
$patients = $db->query("SELECT * FROM patients ORDER BY id DESC LIMIT 50")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h3 class="fw-bold text-secondary"><?php echo __('queue_management'); ?></h3>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="queue_display.php" target="_blank" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-tv"></i> <?php echo __('open_tv_display'); ?>
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-success shadow-sm text-white" data-bs-toggle="modal" data-bs-target="#addTokenModal">
|
||||||
|
<i class="bi bi-plus-lg me-1"></i> <?php echo __('issue_token'); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="card shadow-sm border-0 mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="queueFilterForm" class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<select id="filterDepartment" class="form-select">
|
||||||
|
<option value=""><?php echo __('all_departments'); ?></option>
|
||||||
|
<?php foreach ($departments as $dept): ?>
|
||||||
|
<option value="<?php echo $dept['id']; ?>"><?php echo $dept['name_' . $lang]; ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<select id="filterStatus" class="form-select">
|
||||||
|
<option value=""><?php echo __('all_statuses'); ?></option>
|
||||||
|
<option value="waiting"><?php echo __('waiting'); ?></option>
|
||||||
|
<option value="serving"><?php echo __('serving'); ?></option>
|
||||||
|
<option value="completed"><?php echo __('completed'); ?></option>
|
||||||
|
<option value="cancelled"><?php echo __('cancelled'); ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<button type="button" class="btn btn-secondary w-100" onclick="fetchQueue()"><?php echo __('refresh'); ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Queue List -->
|
||||||
|
<div class="row" id="queueContainer">
|
||||||
|
<!-- Queue cards/table will be injected here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Token Modal -->
|
||||||
|
<div class="modal fade" id="addTokenModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><?php echo __('issue_new_token'); ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="addTokenForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="tokenPatient" class="form-label"><?php echo __('patient'); ?></label>
|
||||||
|
<select class="form-select" id="tokenPatient" required>
|
||||||
|
<option value=""><?php echo __('select_patient'); ?></option>
|
||||||
|
<?php foreach ($patients as $p): ?>
|
||||||
|
<option value="<?php echo $p['id']; ?>"><?php echo $p['name']; ?> (<?php echo $p['phone']; ?>)</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<div class="form-text text-muted"><?php echo __('showing_last_50_patients'); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="tokenDepartment" class="form-label"><?php echo __('department'); ?></label>
|
||||||
|
<select class="form-select" id="tokenDepartment" required>
|
||||||
|
<option value=""><?php echo __('select_department'); ?></option>
|
||||||
|
<?php foreach ($departments as $dept): ?>
|
||||||
|
<option value="<?php echo $dept['id']; ?>"><?php echo $dept['name_' . $lang]; ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="tokenDoctor" class="form-label"><?php echo __('doctor_optional'); ?></label>
|
||||||
|
<select class="form-select" id="tokenDoctor">
|
||||||
|
<option value=""><?php echo __('any_doctor'); ?></option>
|
||||||
|
<?php foreach ($doctors as $doc): ?>
|
||||||
|
<option value="<?php echo $doc['id']; ?>" data-dept="<?php echo $doc['department_id']; ?>"><?php echo $doc['name_' . $lang]; ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo __('cancel'); ?></button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="issueToken()"><?php echo __('issue_token'); ?></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
fetchQueue();
|
||||||
|
|
||||||
|
// Doctor filter logic based on department
|
||||||
|
document.getElementById('tokenDepartment').addEventListener('change', function() {
|
||||||
|
const deptId = this.value;
|
||||||
|
const doctorSelect = document.getElementById('tokenDoctor');
|
||||||
|
const options = doctorSelect.querySelectorAll('option');
|
||||||
|
|
||||||
|
options.forEach(opt => {
|
||||||
|
if (!opt.value) return; // Skip default
|
||||||
|
if (deptId && opt.getAttribute('data-dept') !== deptId) {
|
||||||
|
opt.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
opt.style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
doctorSelect.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto refresh every 30 seconds
|
||||||
|
setInterval(fetchQueue, 30000);
|
||||||
|
});
|
||||||
|
|
||||||
|
function fetchQueue() {
|
||||||
|
const deptId = document.getElementById('filterDepartment').value;
|
||||||
|
const status = document.getElementById('filterStatus').value;
|
||||||
|
|
||||||
|
let url = 'api/queue.php?action=list&lang=<?php echo $lang; ?>';
|
||||||
|
if (deptId) url += '&department_id=' + deptId;
|
||||||
|
if (status) url += '&status=' + status;
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
renderQueue(data.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderQueue(queue) {
|
||||||
|
const container = document.getElementById('queueContainer');
|
||||||
|
if (!queue.length) {
|
||||||
|
container.innerHTML = '<div class="col-12 text-center text-muted py-5"><?php echo __('no_tokens_found'); ?></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<div class="col-12"><div class="table-responsive"><table class="table table-hover align-middle">';
|
||||||
|
html += '<thead class="table-light"><tr><th><?php echo __('token'); ?></th><th><?php echo __('patient'); ?></th><th><?php echo __('department'); ?></th><th><?php echo __('doctor'); ?></th><th><?php echo __('status'); ?></th><th><?php echo __('wait_time'); ?></th><th class="text-end"><?php echo __('actions'); ?></th></tr></thead><tbody>';
|
||||||
|
|
||||||
|
queue.forEach(q => {
|
||||||
|
let statusBadge = '';
|
||||||
|
switch(q.status) {
|
||||||
|
case 'waiting': statusBadge = '<span class="badge bg-warning text-dark"><?php echo __('waiting'); ?></span>'; break;
|
||||||
|
case 'serving': statusBadge = '<span class="badge bg-success"><?php echo __('serving'); ?></span>'; break;
|
||||||
|
case 'completed': statusBadge = '<span class="badge bg-secondary"><?php echo __('completed'); ?></span>'; break;
|
||||||
|
case 'cancelled': statusBadge = '<span class="badge bg-danger"><?php echo __('cancelled'); ?></span>'; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate wait time
|
||||||
|
const created = new Date(q.created_at);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now - created;
|
||||||
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
|
const waitTime = diffMins + ' min';
|
||||||
|
|
||||||
|
html += `<tr>
|
||||||
|
<td class="fw-bold fs-5">#${q.token_number}</td>
|
||||||
|
<td>${q.patient_name}</td>
|
||||||
|
<td>${q.department_name}</td>
|
||||||
|
<td>${q.doctor_name || '-'}</td>
|
||||||
|
<td>${statusBadge}</td>
|
||||||
|
<td><small class="text-muted">${waitTime}</small></td>
|
||||||
|
<td class="text-end">
|
||||||
|
${getActionButtons(q)}
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</tbody></table></div></div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActionButtons(q) {
|
||||||
|
let btns = '';
|
||||||
|
if (q.status === 'waiting') {
|
||||||
|
btns += `<button class="btn btn-sm btn-success me-1" onclick="updateStatus(${q.id}, 'serving')"><i class="bi bi-megaphone"></i> <?php echo __('call'); ?></button>`;
|
||||||
|
btns += `<button class="btn btn-sm btn-danger" onclick="updateStatus(${q.id}, 'cancelled')"><i class="bi bi-x-circle"></i></button>`;
|
||||||
|
} else if (q.status === 'serving') {
|
||||||
|
btns += `<button class="btn btn-sm btn-primary me-1" onclick="updateStatus(${q.id}, 'completed')"><i class="bi bi-check-circle"></i> <?php echo __('finish'); ?></button>`;
|
||||||
|
}
|
||||||
|
return btns;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus(id, status) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('queue_id', id);
|
||||||
|
formData.append('status', status);
|
||||||
|
|
||||||
|
// Pass current doctor ID if logged in (assuming stored in session/global JS var, but for now we won't strictly enforce it on frontend, API handles logic)
|
||||||
|
// Ideally we should pass 'doctor_id' if the user is a doctor claiming the token.
|
||||||
|
|
||||||
|
fetch('api/queue.php?action=update_status', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
fetchQueue();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + data.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function issueToken() {
|
||||||
|
const patientId = document.getElementById('tokenPatient').value;
|
||||||
|
const deptId = document.getElementById('tokenDepartment').value;
|
||||||
|
const docId = document.getElementById('tokenDoctor').value;
|
||||||
|
|
||||||
|
if (!patientId || !deptId) {
|
||||||
|
alert('Please select patient and department');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('patient_id', patientId);
|
||||||
|
formData.append('department_id', deptId);
|
||||||
|
if (docId) formData.append('doctor_id', docId);
|
||||||
|
|
||||||
|
fetch('api/queue.php?action=add', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const modal = bootstrap.Modal.getInstance(document.getElementById('addTokenModal'));
|
||||||
|
modal.hide();
|
||||||
|
fetchQueue();
|
||||||
|
// Show print dialog or small notification
|
||||||
|
alert('Token Generated: #' + data.token_number);
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + data.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
36
lang.php
36
lang.php
@ -278,6 +278,24 @@ $translations = [
|
|||||||
'printed_on' => 'Printed on',
|
'printed_on' => 'Printed on',
|
||||||
'no_appointments' => 'No appointments found for this date',
|
'no_appointments' => 'No appointments found for this date',
|
||||||
'back' => 'Back',
|
'back' => 'Back',
|
||||||
|
'queue_management' => 'Queue Management',
|
||||||
|
'issue_token' => 'Issue Token',
|
||||||
|
'issue_new_token' => 'Issue New Token',
|
||||||
|
'token' => 'Token',
|
||||||
|
'open_tv_display' => 'Open TV Display',
|
||||||
|
'all_departments' => 'All Departments',
|
||||||
|
'all_statuses' => 'All Statuses',
|
||||||
|
'waiting' => 'Waiting',
|
||||||
|
'serving' => 'Serving',
|
||||||
|
'wait_time' => 'Wait Time',
|
||||||
|
'call' => 'Call',
|
||||||
|
'finish' => 'Finish',
|
||||||
|
'no_tokens_found' => 'No tokens found',
|
||||||
|
'select_patient' => 'Select Patient',
|
||||||
|
'doctor_optional' => 'Doctor (Optional)',
|
||||||
|
'any_doctor' => 'Any Doctor',
|
||||||
|
'showing_last_50_patients' => 'Showing last 50 patients',
|
||||||
|
'queue_display' => 'Queue Display',
|
||||||
],
|
],
|
||||||
'ar' => [
|
'ar' => [
|
||||||
'attachment' => 'المرفق',
|
'attachment' => 'المرفق',
|
||||||
@ -559,5 +577,23 @@ $translations = [
|
|||||||
'printed_on' => 'طبع في',
|
'printed_on' => 'طبع في',
|
||||||
'no_appointments' => 'لا توجد مواعيد لهذا التاريخ',
|
'no_appointments' => 'لا توجد مواعيد لهذا التاريخ',
|
||||||
'back' => 'رجوع',
|
'back' => 'رجوع',
|
||||||
|
'queue_management' => 'إدارة الطوابير',
|
||||||
|
'issue_token' => 'إصدار تذكرة',
|
||||||
|
'issue_new_token' => 'إصدار تذكرة جديدة',
|
||||||
|
'token' => 'رقم التذكرة',
|
||||||
|
'open_tv_display' => 'عرض شاشة التلفاز',
|
||||||
|
'all_departments' => 'كل الأقسام',
|
||||||
|
'all_statuses' => 'كل الحالات',
|
||||||
|
'waiting' => 'في الانتظار',
|
||||||
|
'serving' => 'جاري الخدمة',
|
||||||
|
'wait_time' => 'وقت الانتظار',
|
||||||
|
'call' => 'نداء',
|
||||||
|
'finish' => 'إنهاء',
|
||||||
|
'no_tokens_found' => 'لا توجد تذاكر',
|
||||||
|
'select_patient' => 'اختر المريض',
|
||||||
|
'doctor_optional' => 'الطبيب (اختياري)',
|
||||||
|
'any_doctor' => 'أي طبيب',
|
||||||
|
'showing_last_50_patients' => 'عرض آخر 50 مريض',
|
||||||
|
'queue_display' => 'شاشة الانتظار',
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
14
queue.php
Normal file
14
queue.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
// queue.php
|
||||||
|
require_once __DIR__ . '/includes/layout/header.php';
|
||||||
|
|
||||||
|
$section = 'queue';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<?php include __DIR__ . '/includes/pages/queue_management.php'; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/layout/footer.php';
|
||||||
|
?>
|
||||||
265
queue_display.php
Normal file
265
queue_display.php
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
// Use 'en' as base for structure, but we display both languages.
|
||||||
|
$lang = 'en';
|
||||||
|
|
||||||
|
// Fetch initial data to render skeletal HTML
|
||||||
|
try {
|
||||||
|
$db = db();
|
||||||
|
$departments = $db->query("SELECT * FROM departments")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
die("Database error");
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Queue Display / شاشة الانتظار</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;500;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
font-family: 'Tajawal', sans-serif;
|
||||||
|
overflow: hidden; /* Hide scrollbars for TV feel */
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.dept-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
|
||||||
|
height: 100%;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.dept-header {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 1.3rem; /* Slightly smaller to fit two lines */
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 2px solid #e9ecef;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.serving-section {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
background: #e3f2fd;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center; /* Center vertically */
|
||||||
|
}
|
||||||
|
.serving-label {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #1976d2;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.serving-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center; /* Center items vertically in grid */
|
||||||
|
}
|
||||||
|
.serving-item {
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
.serving-number {
|
||||||
|
font-size: 3.5rem; /* Adjusted for grid */
|
||||||
|
font-weight: 800;
|
||||||
|
color: #0d47a1;
|
||||||
|
line-height: 1;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
.waiting-section {
|
||||||
|
padding: 15px;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px dashed #dee2e6;
|
||||||
|
}
|
||||||
|
.waiting-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #6c757d;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.waiting-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.waiting-item {
|
||||||
|
background: #f1f3f5;
|
||||||
|
color: #495057;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.05); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
.clock {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<h1 class="m-0"><i class="bi bi-hospital"></i> Hospital Queue Status / حالة انتظار المستشفى</h1>
|
||||||
|
<div class="clock" id="clock">00:00:00</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container-fluid p-4">
|
||||||
|
<div class="row g-4" id="departmentsContainer">
|
||||||
|
<!-- Javascript will inject cards here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
function updateClock() {
|
||||||
|
const now = new Date();
|
||||||
|
document.getElementById('clock').innerText = now.toLocaleTimeString();
|
||||||
|
}
|
||||||
|
setInterval(updateClock, 1000);
|
||||||
|
updateClock();
|
||||||
|
|
||||||
|
function fetchQueueStatus() {
|
||||||
|
fetch('api/queue.php?action=list')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
renderDepartments(data.data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Fetch error:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDepartments(queueItems) {
|
||||||
|
// Group by department
|
||||||
|
const depts = {};
|
||||||
|
|
||||||
|
// Initialize with server-side departments to ensure empty ones show up
|
||||||
|
<?php foreach ($departments as $d): ?>
|
||||||
|
depts[<?php echo $d['id']; ?>] = {
|
||||||
|
id: <?php echo $d['id']; ?>,
|
||||||
|
name_en: "<?php echo $d['name_en']; ?>",
|
||||||
|
name_ar: "<?php echo $d['name_ar']; ?>",
|
||||||
|
serving: [],
|
||||||
|
waiting: []
|
||||||
|
};
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
// Populate from queue
|
||||||
|
queueItems.forEach(item => {
|
||||||
|
if (!depts[item.department_id]) return;
|
||||||
|
|
||||||
|
// Augment item with doctor names from API if available
|
||||||
|
item.doctor_name_en = item.doctor_name_en || item.doctor_name; // Fallback
|
||||||
|
item.doctor_name_ar = item.doctor_name_ar || item.doctor_name; // Fallback
|
||||||
|
|
||||||
|
if (item.status === 'serving') {
|
||||||
|
depts[item.department_id].serving.push(item);
|
||||||
|
} else if (item.status === 'waiting') {
|
||||||
|
depts[item.department_id].waiting.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const container = document.getElementById('departmentsContainer');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
Object.values(depts).forEach(dept => {
|
||||||
|
|
||||||
|
let servingHtml = '<div class="text-muted small text-center w-100" style="grid-column: span 2;">No active patient<br>لا يوجد مريض</div>';
|
||||||
|
|
||||||
|
if (dept.serving.length > 0) {
|
||||||
|
servingHtml = dept.serving.map(s => {
|
||||||
|
let docName = '';
|
||||||
|
if (s.doctor_name_en && s.doctor_name_ar) {
|
||||||
|
docName = `${s.doctor_name_en}<br>${s.doctor_name_ar}`;
|
||||||
|
} else {
|
||||||
|
docName = s.doctor_name || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="serving-item">
|
||||||
|
<div class="serving-number">${s.token_number}</div>
|
||||||
|
<div class="small text-muted text-center" style="line-height:1.2; font-size:0.85rem;">${docName}</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
let waitingHtml = '<span class="text-muted small">-</span>';
|
||||||
|
if (dept.waiting.length > 0) {
|
||||||
|
waitingHtml = dept.waiting.map(w => `<span class="waiting-item">${w.token_number}</span>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const card = `
|
||||||
|
<div class="col-md-4 col-lg-3">
|
||||||
|
<div class="dept-card">
|
||||||
|
<div class="dept-header">
|
||||||
|
${dept.name_en}
|
||||||
|
<div class="text-muted small mt-1" style="font-size: 1.1rem; font-weight: 500;">${dept.name_ar}</div>
|
||||||
|
</div>
|
||||||
|
<div class="serving-section">
|
||||||
|
<div class="serving-label">
|
||||||
|
Now Serving<br>
|
||||||
|
<span style="font-size: 0.9em;">جاري الخدمة</span>
|
||||||
|
</div>
|
||||||
|
<div class="serving-list">
|
||||||
|
${servingHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="waiting-section">
|
||||||
|
<div class="waiting-label">Waiting / الانتظار</div>
|
||||||
|
<div class="waiting-list">
|
||||||
|
${waitingHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
container.insertAdjacentHTML('beforeend', card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
fetchQueueStatus();
|
||||||
|
// Poll every 5 seconds
|
||||||
|
setInterval(fetchQueueStatus, 5000);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user