diff --git a/.gemini/GEMINI_LAST_MESSAGE.txt b/.gemini/GEMINI_LAST_MESSAGE.txt
index b08b94e..4f250e6 100644
--- a/.gemini/GEMINI_LAST_MESSAGE.txt
+++ b/.gemini/GEMINI_LAST_MESSAGE.txt
@@ -1,14 +1,19 @@
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.
-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 (`//`).
-3. **Verify:** I verified the file content to ensure the syntax is now correct.
+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. **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. **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:
-* `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:
-* The automatic age-to-DOB calculation (and vice-versa) should now work correctly when you type in the "Age" field.
-* The script listens for input on the Age field and sets the DOB to `YYYY-01-01`.
+* The application should now load correctly without the 500 error.
+* 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.
-Reminder: Click Save in the editor to sync changes.
\ No newline at end of file
+Next: Please refresh your browser and try adding a visit with the "Issue Token" checkbox checked. Let me know if everything works as expected!
\ No newline at end of file
diff --git a/api/queue.php b/api/queue.php
new file mode 100644
index 0000000..2054796
--- /dev/null
+++ b/api/queue.php
@@ -0,0 +1,179 @@
+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()]);
+}
diff --git a/check_patients_table.php b/check_patients_table.php
new file mode 100644
index 0000000..469ea02
--- /dev/null
+++ b/check_patients_table.php
@@ -0,0 +1,9 @@
+query("DESCRIBE patients");
+$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+foreach ($rows as $row) {
+ echo $row['Field'] . "\n";
+}
+
diff --git a/db/migrations/20260316_create_patient_queue.sql b/db/migrations/20260316_create_patient_queue.sql
new file mode 100644
index 0000000..6ac5844
--- /dev/null
+++ b/db/migrations/20260316_create_patient_queue.sql
@@ -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);
diff --git a/debug_queue.php b/debug_queue.php
new file mode 100644
index 0000000..ebebf53
--- /dev/null
+++ b/debug_queue.php
@@ -0,0 +1,37 @@
+Patient Queue (Latest 5)";
+$stmt = $pdo->query("SELECT * FROM patient_queue ORDER BY id DESC LIMIT 5");
+$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+if ($rows) {
+ echo "
";
+ foreach (array_keys($rows[0]) as $k) echo "| $k | ";
+ echo "
";
+ foreach ($rows as $row) {
+ echo "";
+ foreach ($row as $v) echo "| $v | ";
+ echo "
";
+ }
+ echo "
";
+} else {
+ echo "No records in patient_queue.
";
+}
+
+echo "Visits (Latest 5)
";
+$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 "";
+ foreach (array_keys($rows[0]) as $k) echo "| $k | ";
+ echo "
";
+ foreach ($rows as $row) {
+ echo "";
+ foreach ($row as $v) echo "| $v | ";
+ echo "
";
+ }
+ echo "
";
+} else {
+ echo "No records in visits.
";
+}
diff --git a/includes/layout/header.php b/includes/layout/header.php
index 681b887..9c7674d 100644
--- a/includes/layout/header.php
+++ b/includes/layout/header.php
@@ -102,6 +102,7 @@ $site_favicon = !empty($site_settings['company_favicon']) ? $site_settings['comp
+