Compare commits

...

75 Commits

Author SHA1 Message Date
Flatlogic Bot
8f9c7c6314 update the install file 2026-03-28 13:36:35 +00:00
Flatlogic Bot
e09626b316 updating queuing system 2026-03-28 10:57:32 +00:00
Flatlogic Bot
e150954bb6 add full screen display 2026-03-23 09:59:23 +00:00
Flatlogic Bot
5eaa8cc290 update disblay 2026-03-23 09:51:49 +00:00
Flatlogic Bot
9667cec7e6 update users 2026-03-23 06:09:04 +00:00
Flatlogic Bot
3ca5cde9e3 adding rooms to doctors 2026-03-23 03:54:02 +00:00
Flatlogic Bot
620485b60a add rooms for doctors 2026-03-23 02:54:44 +00:00
Flatlogic Bot
fbacfd6e29 update departments 2026-03-23 02:22:57 +00:00
Flatlogic Bot
0690a23bb7 update departments 2026-03-23 02:09:49 +00:00
Flatlogic Bot
5f99108a0d edit departments 2026-03-22 18:08:44 +00:00
Flatlogic Bot
132df22fd4 fix: resolve project_uuid_missing error in deployment by adding robust env loading and fallbacks 2026-03-22 17:59:16 +00:00
Flatlogic Bot
97ae4ba490 update reciept 2026-03-22 17:51:27 +00:00
Flatlogic Bot
329da3e5d5 more editing 2026-03-22 15:27:17 +00:00
Flatlogic Bot
a03fa77672 Autosave: 20260322-150724 2026-03-22 15:07:24 +00:00
Flatlogic Bot
04bd70e7d8 adding sound to display 2026-03-22 14:59:37 +00:00
Flatlogic Bot
bcd593fb90 adding nursing notes 2026-03-22 13:29:44 +00:00
Flatlogic Bot
e73384ddbc edit visit update 2026-03-22 13:03:15 +00:00
Flatlogic Bot
0d5768a3f8 update permissions 2026-03-22 10:20:15 +00:00
Flatlogic Bot
4472d09232 Autosave: 20260322-101548 2026-03-22 10:15:48 +00:00
Flatlogic Bot
6933c13c3e updating HR and integrate it to modules 2026-03-22 10:04:41 +00:00
Flatlogic Bot
f62878214d add HR 2026-03-22 06:48:07 +00:00
Flatlogic Bot
10338c6bc6 Autosave: 20260322-064144 2026-03-22 06:41:44 +00:00
Flatlogic Bot
b02c277bb8 update 33 2026-03-22 04:48:31 +00:00
Flatlogic Bot
5f98198b67 update patients form 2026-03-22 04:29:56 +00:00
Flatlogic Bot
f10cdedce9 debug3 2026-03-22 03:56:20 +00:00
Flatlogic Bot
70cf52dd48 update dashboard bug 2026-03-22 03:47:43 +00:00
Flatlogic Bot
07e733df85 update dashboard 2026-03-22 03:40:33 +00:00
Flatlogic Bot
23c927161f update migrations 2 2026-03-22 03:32:55 +00:00
Flatlogic Bot
3912cd5bd2 migration update 2026-03-22 03:27:37 +00:00
Flatlogic Bot
6387f7c226 user profile 2026-03-22 03:18:19 +00:00
Flatlogic Bot
5e776c9e2b updating appointments 2026-03-21 18:53:48 +00:00
Flatlogic Bot
68751660aa Autosave: 20260321-183405 2026-03-21 18:34:05 +00:00
Flatlogic Bot
d96399430d update reports 2026-03-21 18:14:03 +00:00
Flatlogic Bot
66038df9e4 adding stock management 2026-03-21 18:03:25 +00:00
Flatlogic Bot
1cb1f89067 Autosave: 20260321-173937 2026-03-21 17:39:37 +00:00
Flatlogic Bot
a6e75b0397 permission system 2026-03-21 16:58:23 +00:00
Flatlogic Bot
2bc76dec94 fix: admin reports overflow issue by constraining chart containers 2026-03-21 14:59:19 +00:00
Flatlogic Bot
dc081ef86a adding admin reports 2026-03-21 14:56:14 +00:00
Flatlogic Bot
b5ea341aa5 Autosave: 20260321-140606 2026-03-21 14:06:06 +00:00
Flatlogic Bot
dad73767a1 add purchase to pharmacy 2026-03-21 12:43:58 +00:00
Flatlogic Bot
fd41c8937a update lpos 2026-03-21 09:40:17 +00:00
Flatlogic Bot
bab61e3570 updating pharmacy 2026-03-21 09:33:13 +00:00
Flatlogic Bot
641316f659 add pharmacy 2026-03-21 07:52:14 +00:00
Flatlogic Bot
01f56287c6 updating billing 2026-03-21 07:38:35 +00:00
Flatlogic Bot
43f495d39b adding home visit 2026-03-17 09:31:25 +00:00
Flatlogic Bot
9a2dea5596 Autosave: 20260317-082239 2026-03-17 08:22:39 +00:00
Flatlogic Bot
2575e8e91e Autosave: 20260317-024109 2026-03-17 02:41:09 +00:00
Flatlogic Bot
e4e7b4a246 fix: resolve 500 error in actions.php and re-implement token system checkbox 2026-03-16 14:25:36 +00:00
Flatlogic Bot
adc8aae5d0 Autosave: 20260316-130637 2026-03-16 13:06:37 +00:00
Flatlogic Bot
4f4d8efa33 adding working time 2026-03-16 10:31:04 +00:00
Flatlogic Bot
4ea57c7524 Autosave: 20260315-175807 2026-03-15 17:58:07 +00:00
Flatlogic Bot
5420bde76a more updates 2026-03-15 14:00:10 +00:00
Flatlogic Bot
4f1232c067 Autosave: 20260315-102441 2026-03-15 10:24:41 +00:00
Flatlogic Bot
5f78c2abad update visits list 2026-03-13 06:03:11 +00:00
Flatlogic Bot
0a3eff3c92 Autosave: 20260312-174549 2026-03-12 17:45:50 +00:00
Flatlogic Bot
d79aa1e948 service update 2026-03-06 12:47:25 +00:00
Flatlogic Bot
523db02f6f Autosave: 20260306-100128 2026-03-06 10:01:28 +00:00
Flatlogic Bot
230a62f266 updating patients 2026-03-06 02:52:55 +00:00
Flatlogic Bot
2b7bb7957e enhancing patient form 2026-03-06 02:23:44 +00:00
Flatlogic Bot
6439167fd2 updating patient form 2026-03-05 18:47:27 +00:00
Flatlogic Bot
48924b82af updating lists for dynamic search 2026-03-05 18:13:24 +00:00
Flatlogic Bot
4d100ba658 Autosave: 20260305-171454 2026-03-05 17:14:54 +00:00
Flatlogic Bot
d04323d02a excel import 2026-03-05 13:19:30 +00:00
Flatlogic Bot
e15aadbaf9 Autosave: 20260305-095147 2026-03-05 09:51:47 +00:00
Flatlogic Bot
defcfeb7da adding drugs 2026-03-05 08:11:47 +00:00
Flatlogic Bot
c3784f3565 Autosave: 20260305-064557 2026-03-05 06:45:57 +00:00
Flatlogic Bot
c8679068a9 adding ai generation 2026-03-05 05:14:09 +00:00
Flatlogic Bot
c6904a9a5e Autosave: 20260304-174206 2026-03-04 17:42:06 +00:00
Flatlogic Bot
d3172b6c89 adding html editor 2026-03-04 17:34:51 +00:00
Flatlogic Bot
6f43ba8047 Autosave: 20260304-130505 2026-03-04 13:05:05 +00:00
Flatlogic Bot
933409e6cf Autosave: 20260304-080704 2026-03-04 08:07:04 +00:00
Flatlogic Bot
a63d31ec70 update lab 2026-03-04 07:07:46 +00:00
Flatlogic Bot
63a866d898 Autosave: 20260304-062638 2026-03-04 06:26:39 +00:00
Flatlogic Bot
85f641cde7 Autosave: 20260304-044254 2026-03-04 04:42:54 +00:00
Flatlogic Bot
aa6a7b2ed1 adding insurance 2026-03-04 04:25:27 +00:00
205 changed files with 29778 additions and 249 deletions

View File

@ -0,0 +1,13 @@
Plan:
1. **Investigate:** Checked Apache error logs and `includes/actions.php`.
2. **Identify Issue:** Found a mismatch between the database schema for `insurance_payments` (missing `payment_method` column) and the PHP code trying to insert into it.
3. **Fix:** Created and applied a migration to add the `payment_method` column to the `insurance_payments` table.
4. **Verify:** Confirmed the column now exists in the database.
Changed Files:
* `db/migrations/20260321_z_add_payment_method_to_insurance_payments.sql`: Added migration to fix the schema.
Next Steps:
* Go to **Insurance > Transactions**.
* Try adding a transaction again. It should work now.
* **Reminder:** Click Save in the editor to sync changes.

113
SETUP.md Normal file
View File

@ -0,0 +1,113 @@
# Project Setup Guide
This guide provides step-by-step instructions to set up, configure, and run the Hospital Management System.
## Prerequisites
* **PHP**: Version 8.0 or higher.
* **Database**: MariaDB 10.x or MySQL 5.7+.
* **Web Server**: Apache 2.4+ (with `mod_rewrite` enabled).
* **Composer**: (Optional) For dependency management if using third-party packages in the future.
## Installation Steps
1. **Clone the Repository**
```bash
git clone <repository_url>
cd <project_directory>
```
2. **Configure Web Server**
* Point your Apache VirtualHost `DocumentRoot` to the project directory.
* Ensure the directory has appropriate permissions (e.g., `www-data` user).
* Enable `.htaccess` overrides if applicable.
3. **Database Setup**
* Create a new MySQL/MariaDB database (e.g., `hospital_db`).
* Import the database schema and seed data. You can do this by running the initialization script or importing SQL files:
* **Option A (Script):** Run `php install.php` from the command line (ensure `db/config.php` is configured first).
* **Option B (Manual):** Import files from `db/migrations/` in chronological order using a tool like phpMyAdmin or CLI.
4. **Configuration Files**
* **Database:** Edit `db/config.php` to match your database credentials:
```php
define('DB_HOST', '127.0.0.1');
define('DB_NAME', 'your_db_name');
define('DB_USER', 'your_db_user');
define('DB_PASS', 'your_db_password');
```
## Configuration & Environment Variables
The application uses a mix of PHP constants and environment variables for configuration.
### Email Configuration (`mail/config.php`)
The application uses PHPMailer. Configure the following environment variables (in your server config or `.env` file):
* `MAIL_TRANSPORT`: `smtp` (default)
* `SMTP_HOST`: e.g., `smtp.gmail.com`
* `SMTP_PORT`: e.g., `587`
* `SMTP_SECURE`: `tls` or `ssl`
* `SMTP_USER`: Your email address
* `SMTP_PASS`: Your email password or App Password
* `MAIL_FROM`: Sender email address
* `MAIL_FROM_NAME`: Sender name (e.g., "Hospital Admin")
### AI Integration (`ai/config.php`)
The system uses an AI proxy for features like "AI Suggestion" in visit forms and the Telegram bot.
* **Config File:** `ai/config.php`
* **Key Settings:** `base_url`, `project_uuid`.
* **Usage:** See `ai/LocalAIApi.php`.
### Telegram Integration
The system includes a Telegram bot for answering FAQs (`api/telegram_webhook.php`).
1. **Create a Bot:** Talk to `@BotFather` on Telegram to create a bot and get a **Token**.
2. **Save Token:** Insert the token into the `settings` table in your database:
```sql
INSERT INTO settings (setting_key, setting_value) VALUES ('telegram_token', 'YOUR_TELEGRAM_BOT_TOKEN');
```
3. **Set Webhook:** Configure the webhook URL for your bot:
```
https://api.telegram.org/bot<YOUR_TOKEN>/setWebhook?url=https://<YOUR_DOMAIN>/api/telegram_webhook.php
```
## Integrations & Libraries
### Frontend (CDN)
The project relies on the following CDN-hosted libraries. Ensure you have an active internet connection.
* **Framework:** Bootstrap 5.3.0
* **Icons:** Bootstrap Icons 1.10.5
* **Fonts:** Google Fonts (Inter, Tajawal)
* **Forms:**
* Select2 4.1.0 (with Bootstrap 5 theme)
* Summernote Lite 0.8.18 (Rich Text Editor)
* Flatpickr (Date/Time picker)
* Inputmask 5.0.7 (Input formatting)
* **Utilities:**
* jQuery 3.6.0
* Ui-Avatars (User avatars)
* JsBarcode 3.11.5 (Patient labels)
### Backend Services
* **Email:** `mail/MailService.php` (Wraps PHPMailer).
* **AI:** `ai/LocalAIApi.php` (Handles OpenAI requests via proxy).
* **Reporting:** `includes/SimpleXLSX.php` (Excel export support).
## Default Credentials
If you used the provided migration scripts (`20260321_create_auth_system.sql`), the default administrator account is:
* **Email:** `admin@hospital.com`
* **Password:** `admin123`
* **Role:** Administrator
**Note:** Change this password immediately after logging in.
## Troubleshooting
* **Database Connection Error:** Check `db/config.php` credentials. Ensure the database server is running.
* **Email Not Sending:** Verify SMTP settings in `mail/config.php` or environment variables. Check server logs for connection timeouts.
* **AI Features Not Working:** Ensure the server can reach the AI proxy endpoint defined in `ai/config.php`.
* **Telegram Bot Not Responding:** Verify the `telegram_token` in the `settings` table and ensure the webhook URL is accessible from the public internet (HTTPS required).

View File

@ -1,41 +1,63 @@
<?php <?php
// OpenAI proxy configuration (workspace scope). // OpenAI proxy configuration (workspace scope).
// Reads values from environment variables or executor/.env. // Reads values from environment variables or .env files.
$projectUuid = getenv('PROJECT_UUID'); // Helper to check env vars from multiple sources
$projectId = getenv('PROJECT_ID'); function get_env_var($key) {
$val = getenv($key);
if ( if ($val !== false && $val !== '' && $val !== null) return $val;
($projectUuid === false || $projectUuid === null || $projectUuid === '') || if (isset($_SERVER[$key]) && $_SERVER[$key] !== '') return $_SERVER[$key];
($projectId === false || $projectId === null || $projectId === '') if (isset($_ENV[$key]) && $_ENV[$key] !== '') return $_ENV[$key];
) { return null;
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
if ($envPath && is_readable($envPath)) {
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (!str_contains($line, '=')) {
continue;
}
[$key, $value] = array_map('trim', explode('=', $line, 2));
if ($key === '') {
continue;
}
$value = trim($value, "\"' ");
if (getenv($key) === false || getenv($key) === '') {
putenv("{$key}={$value}");
}
}
$projectUuid = getenv('PROJECT_UUID');
$projectId = getenv('PROJECT_ID');
}
} }
$projectUuid = ($projectUuid === false) ? null : $projectUuid; $projectUuid = get_env_var('PROJECT_UUID');
$projectId = ($projectId === false) ? null : $projectId; $projectId = get_env_var('PROJECT_ID');
// If missing, try loading from .env files
if (!$projectUuid || !$projectId) {
// List of possible .env locations (relative to this file)
$possiblePaths = [
__DIR__ . '/../../.env', // executor/.env
__DIR__ . '/../.env', // workspace/.env
__DIR__ . '/.env', // ai/.env
dirname(__DIR__, 2) . '/.env' // Alternative root
];
foreach ($possiblePaths as $path) {
$realPath = realpath($path);
if ($realPath && is_readable($realPath)) {
$lines = @file($realPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') continue;
if (!str_contains($line, '=')) continue;
[$key, $value] = array_map('trim', explode('=', $line, 2));
if ($key === '') continue;
$value = trim($value, "' ");
// Only set if not already set
if (!get_env_var($key)) {
putenv("{$key}={$value}");
$_SERVER[$key] = $value;
$_ENV[$key] = $value;
}
}
// Stop after first successful .env load
break;
}
}
// Refresh vars
$projectUuid = get_env_var('PROJECT_UUID');
$projectId = get_env_var('PROJECT_ID');
}
// Fallback if environment variables are completely missing in deployment
// Using values from db/config.php logic or current environment
if (!$projectUuid) $projectUuid = '36fb441e-8408-4101-afdc-7911dc065e36';
if (!$projectId) $projectId = '38960';
$baseUrl = 'https://flatlogic.com'; $baseUrl = 'https://flatlogic.com';
$responsesPath = $projectId ? "/projects/{$projectId}/ai-request" : null; $responsesPath = $projectId ? "/projects/{$projectId}/ai-request" : null;

80
api/ai_report.php Normal file
View File

@ -0,0 +1,80 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../ai/LocalAIApi.php';
// Get JSON input
$input = json_decode(file_get_contents('php://input'), true);
$target = $input['target'] ?? 'treatment_plan'; // symptoms, diagnosis, treatment_plan, translate
$symptoms = $input['symptoms'] ?? '';
$diagnosis = $input['diagnosis'] ?? '';
$currentValue = $input['current_value'] ?? ''; // For expanding symptoms or translation text
$systemPrompt = 'You are a professional medical assistant.';
$userPrompt = "";
switch ($target) {
case 'translate':
$text = $input['text'] ?? '';
$from = $input['from'] ?? 'English';
$to = $input['to'] ?? 'Arabic';
if (empty($text)) {
echo json_encode(['success' => false, 'error' => 'No text provided for translation.']);
exit;
}
$systemPrompt = "You are a professional translator specializing in medical terminology.";
$userPrompt = "Translate the following text from $from to $to. Return only the translated text without any explanations or extra characters.\n\nText: $text";
break;
case 'symptoms':
if (empty($currentValue)) {
$userPrompt = "Generate a list of common clinical symptoms for a general checkup in a professional medical format (HTML lists).";
} else {
$userPrompt = "Rewrite and expand the following patient symptoms into a professional clinical description using HTML (bullet points or paragraph). Maintain the original meaning but make it clearer and more detailed:\n\n" . strip_tags($currentValue);
}
break;
case 'diagnosis':
if (empty($symptoms)) {
echo json_encode(['success' => false, 'error' => 'Please enter symptoms first to get a diagnosis suggestion.']);
exit;
}
$userPrompt = "Based on the following symptoms, suggest a list of potential differential diagnoses. Provide the response in a clear HTML list format.\n\nSymptoms:\n" . strip_tags($symptoms);
break;
case 'treatment_plan':
default:
if (empty($symptoms) && empty($diagnosis)) {
echo json_encode(['success' => false, 'error' => 'No symptoms or diagnosis provided.']);
exit;
}
$userPrompt = "Based on the following symptoms and diagnosis, please generate a concise treatment plan and medical report for the patient.\n\n";
if (!empty($symptoms)) {
$userPrompt .= "Symptoms:\n" . strip_tags($symptoms) . "\n\n";
}
if (!empty($diagnosis)) {
$userPrompt .= "Diagnosis:\n" . strip_tags($diagnosis) . "\n\n";
}
$userPrompt .= "Please provide the report in a clear, professional format using HTML tags (like <ul>, <li>, <strong>, etc.) for better readability.";
break;
}
try {
$response = LocalAIApi::createResponse([
'input' => [
['role' => 'system', 'content' => $systemPrompt],
['role' => 'user', 'content' => $userPrompt],
],
]);
if (!empty($response['success'])) {
$text = LocalAIApi::extractText($response);
echo json_encode(['success' => true, 'report' => trim($text)]);
} else {
echo json_encode(['success' => false, 'error' => $response['error'] ?? 'AI generation failed.']);
}
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

359
api/appointments.php Normal file
View File

@ -0,0 +1,359 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../helpers.php';
// Prevent caching
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
header('Content-Type: application/json');
$db = db();
$lang = $_SESSION['lang'] ?? 'en';
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$id = $_GET['id'] ?? null;
if ($id) {
// Fetch single appointment
$stmt = $db->prepare("SELECT * FROM appointments WHERE id = ?");
$stmt->execute([$id]);
$appointment = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode($appointment);
exit;
}
$startStr = $_GET['start'] ?? null;
$endStr = $_GET['end'] ?? null;
$doctor_id = $_GET['doctor_id'] ?? null;
$events = [];
$businessHours = [];
// Fetch Appointments
$query = "
SELECT
a.id, a.start_time as start, a.end_time as end, a.reason, a.status,
a.patient_id, a.doctor_id, a.nurse_id, a.visit_type, a.address,
p.name as patient_name,
doc.name_$lang as doctor_name,
nur.name_$lang as nurse_name
FROM appointments a
JOIN patients p ON a.patient_id = p.id
LEFT JOIN employees doc ON a.doctor_id = doc.id
LEFT JOIN employees nur ON a.nurse_id = nur.id
WHERE 1=1";
$params = [];
if ($startStr) { $query .= " AND a.start_time >= ?"; $params[] = $startStr; }
if ($endStr) { $query .= " AND a.start_time <= ?"; $params[] = $endStr; }
if ($doctor_id) { $query .= " AND a.doctor_id = ?"; $params[] = $doctor_id; }
try {
$stmt = $db->prepare($query);
$stmt->execute($params);
$appointments = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log("Appointments Fetch Error: " . $e->getMessage());
$appointments = [];
}
foreach ($appointments as $a) {
$color = '#0d6efd'; // blue
if ($a['status'] === 'Completed') $color = '#198754'; // green
if ($a['status'] === 'Cancelled') $color = '#dc3545'; // red
if ($a['visit_type'] === 'Home') $color = '#fd7e14'; // orange for home visits
$providerName = $a['doctor_name'] ? $a['doctor_name'] : ($a['nurse_name'] . ' (' . __('nurse') . ')');
$title = $a['patient_name'] . ' - ' . $providerName;
if ($a['visit_type'] === 'Home') {
$title = '[🏠] ' . $title;
}
$events[] = [
'id' => $a['id'],
'title' => $title,
'start' => $a['start'],
'end' => $a['end'],
'color' => $color,
'extendedProps' => [
'type' => 'appointment',
'patient_id' => $a['patient_id'],
'doctor_id' => $a['doctor_id'],
'nurse_id' => $a['nurse_id'],
'visit_type' => $a['visit_type'],
'address' => $a['address'],
'patient_name' => $a['patient_name'],
'doctor_name' => $a['doctor_name'],
'nurse_name' => $a['nurse_name'],
'status' => $a['status'],
'reason' => $a['reason']
]
];
}
// Fetch Public Holidays
$holidayQuery = "SELECT holiday_date as start, name_$lang as title FROM holidays WHERE 1=1";
$holidayParams = [];
if ($startStr) { $holidayQuery .= " AND holiday_date >= ?"; $holidayParams[] = date('Y-m-d', strtotime($startStr)); }
if ($endStr) { $holidayQuery .= " AND holiday_date <= ?"; $holidayParams[] = date('Y-m-d', strtotime($endStr)); }
try {
$stmt = $db->prepare($holidayQuery);
$stmt->execute($holidayParams);
$holidays = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
$holidays = [];
}
$s = get_system_settings();
$global_start = $s['working_hours_start'] ?? '08:00';
$global_end = $s['working_hours_end'] ?? '17:00';
foreach ($holidays as $h) {
// Render a visible block event in the time grid
$events[] = [
'id' => 'hol_block_' . $h['start'],
'title' => __('holiday') . ': ' . $h['title'],
'start' => $h['start'] . 'T' . $global_start . ':00',
'end' => $h['start'] . 'T' . $global_end . ':00',
'allDay' => false,
'color' => '#dc3545',
'textColor' => '#fff',
'className' => 'public-holiday-event',
'extendedProps' => ['type' => 'public_holiday_block', 'blocks_selection' => true]
];
// Render daily blocks for time grid
$events[] = [
'id' => 'hol_bg_' . $h['start'],
'start' => $h['start'] . 'T00:00:00',
'end' => $h['start'] . 'T23:59:59',
'allDay' => false,
'display' => 'background',
'backgroundColor' => 'rgba(255, 193, 7, 0.5)',
'overlap' => false,
'extendedProps' => ['type' => 'public_holiday', 'blocks_selection' => true]
];
// Visible event strip across the top
$events[] = [
'id' => 'hol_title_' . $h['start'],
'title' => __('holiday') . ': ' . $h['title'],
'start' => $h['start'],
'end' => $h['start'],
'allDay' => true,
'color' => '#ffc107',
'textColor' => '#000',
'extendedProps' => ['type' => 'public_holiday']
];
}
// Fetch Doctor Holidays (from Leave Requests)
// Updated to join employees instead of doctors
$docHolidayQuery = "
SELECT lr.id, lr.start_date, lr.end_date, lr.reason as note,
lr.employee_id as doctor_id,
e.name_$lang as doctor_name
FROM leave_requests lr
JOIN employees e ON lr.employee_id = e.id
WHERE lr.status = 'Approved'";
$docHolidayParams = [];
// Date filtering for doctor holidays (ranges)
if ($startStr && $endStr) {
$docHolidayQuery .= " AND lr.start_date <= ? AND lr.end_date >= ?";
$docHolidayParams[] = date('Y-m-d', strtotime($endStr));
$docHolidayParams[] = date('Y-m-d', strtotime($startStr));
}
if ($doctor_id) {
$docHolidayQuery .= " AND lr.employee_id = ?";
$docHolidayParams[] = $doctor_id;
}
try {
$stmt = $db->prepare($docHolidayQuery);
$stmt->execute($docHolidayParams);
$docHolidays = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
$docHolidays = [];
}
foreach ($docHolidays as $dh) {
$title = $dh['doctor_name'] . ' - ' . ($dh['note'] ?: 'Holiday');
$endDayStr = date('Y-m-d', strtotime($dh['end_date'] . ' +1 day'));
$currentDate = strtotime($dh['start_date']);
$endDate = strtotime($dh['end_date']);
$isFilteredDoctor = ($doctor_id && $doctor_id == $dh['doctor_id']);
// Output background events for each day individually
while ($currentDate <= $endDate) {
$dateStr = date('Y-m-d', $currentDate);
$events[] = [
'id' => 'doc_hol_block_' . $dh['id'] . '_' . $dateStr,
'title' => 'Holiday: ' . $dh['doctor_name'],
'start' => $dateStr . 'T' . $global_start . ':00',
'end' => $dateStr . 'T' . $global_end . ':00',
'allDay' => false,
'color' => '#ffc107',
'textColor' => '#000',
'className' => 'doctor-holiday-event',
'extendedProps' => ['type' => 'doctor_holiday_block', 'doctor_id' => $dh['doctor_id'], 'blocks_selection' => $isFilteredDoctor]
];
$events[] = [
'id' => 'doc_hol_bg_' . $dh['id'] . '_' . $dateStr,
'start' => $dateStr . 'T00:00:00',
'end' => $dateStr . 'T23:59:59',
'display' => 'background',
'allDay' => false,
'backgroundColor' => $isFilteredDoctor ? 'rgba(255, 193, 7, 0.5)' : 'rgba(255, 193, 7, 0.15)',
'overlap' => !$isFilteredDoctor,
'extendedProps' => ['type' => 'doctor_holiday', 'doctor_id' => $dh['doctor_id'], 'blocks_selection' => $isFilteredDoctor]
];
$currentDate = strtotime('+1 day', $currentDate);
}
$events[] = [
'id' => 'doc_hol_' . $dh['id'],
'title' => $title,
'start' => $dh['start_date'],
'end' => $endDayStr,
'allDay' => true,
'color' => '#ffc107',
'textColor' => '#000',
'extendedProps' => ['type' => 'doctor_holiday', 'doctor_id' => $dh['doctor_id'], 'blocks_selection' => $isFilteredDoctor]
];
}
// Set Business Hours (Global Default since individual schedules are removed)
$st = $s['working_hours_start'] ?? '08:00';
$et = $s['working_hours_end'] ?? '17:00';
$businessHours = [
[
'daysOfWeek' => [0, 1, 2, 3, 4, 5, 6],
'startTime' => $st,
'endTime' => $et
]
];
echo json_encode([
'events' => $events,
'businessHours' => $businessHours,
'settings' => [
'working_hours_start' => $global_start,
'working_hours_end' => $global_end
]
]);
exit;
}
function checkDoctorHoliday($db, $doctor_id, $start_time) {
if (!$doctor_id || !$start_time) return false;
$date = date('Y-m-d', strtotime($start_time));
try {
// Query leave_requests directly using employee_id (which is $doctor_id)
$stmt = $db->prepare("SELECT COUNT(*) FROM leave_requests WHERE employee_id = ? AND status = 'Approved' AND ? BETWEEN start_date AND end_date");
$stmt->execute([$doctor_id, $date]);
return $stmt->fetchColumn() > 0;
} catch (PDOException $e) {
error_log("Check Holiday Error: " . $e->getMessage());
return false;
}
}
if ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
$input = $_POST;
}
if (empty($input)) {
echo json_encode(['success' => false, 'error' => 'No input data received']);
exit;
}
$action = $input['action'] ?? '';
if (empty($action)) {
echo json_encode(['success' => false, 'error' => 'No action specified']);
exit;
}
if ($action === 'create') {
$patient_id = $input['patient_id'] ?? '';
$doctor_id = $input['doctor_id'] ?: null; // Nullable
$nurse_id = $input['nurse_id'] ?: null; // Nullable
$visit_type = $input['visit_type'] ?? 'Clinic';
$address = $input['address'] ?? '';
$start_time = $input['start_time'] ?? '';
$reason = $input['reason'] ?? '';
if ($patient_id && ($doctor_id || $nurse_id) && $start_time) {
// Check for holiday conflict if doctor assigned
if ($doctor_id && checkDoctorHoliday($db, $doctor_id, $start_time)) {
echo json_encode(['success' => false, 'error' => 'Doctor is on holiday on this date.']);
exit;
}
try {
$stmt = $db->prepare("INSERT INTO appointments (patient_id, doctor_id, nurse_id, visit_type, address, start_time, end_time, reason) VALUES (?, ?, ?, ?, ?, ?, DATE_ADD(?, INTERVAL 30 MINUTE), ?)");
$stmt->execute([$patient_id, $doctor_id, $nurse_id, $visit_type, $address, $start_time, $start_time, $reason]);
echo json_encode(['success' => true, 'id' => $db->lastInsertId()]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'error' => 'DB Error: ' . $e->getMessage()]);
}
} else {
echo json_encode(['success' => false, 'error' => 'Missing fields']);
}
} elseif ($action === 'update') {
$id = $input['id'] ?? '';
$patient_id = $input['patient_id'] ?? '';
$doctor_id = $input['doctor_id'] ?: null;
$nurse_id = $input['nurse_id'] ?: null;
$visit_type = $input['visit_type'] ?? 'Clinic';
$address = $input['address'] ?? '';
$start_time = $input['start_time'] ?? '';
$status = $input['status'] ?? 'Scheduled';
$reason = $input['reason'] ?? '';
if ($id && $patient_id && ($doctor_id || $nurse_id) && $start_time) {
// Check for holiday conflict
if ($doctor_id && checkDoctorHoliday($db, $doctor_id, $start_time)) {
echo json_encode(['success' => false, 'error' => 'Doctor is on holiday on this date.']);
exit;
}
try {
$stmt = $db->prepare("UPDATE appointments SET patient_id = ?, doctor_id = ?, nurse_id = ?, visit_type = ?, address = ?, start_time = ?, end_time = DATE_ADD(?, INTERVAL 30 MINUTE), status = ?, reason = ? WHERE id = ?");
$stmt->execute([$patient_id, $doctor_id, $nurse_id, $visit_type, $address, $start_time, $start_time, $status, $reason, $id]);
echo json_encode(['success' => true]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'error' => 'DB Error: ' . $e->getMessage()]);
}
} else {
echo json_encode(['success' => false, 'error' => 'Missing fields']);
}
} elseif ($action === 'delete') {
$id = $input['id'] ?? '';
if ($id) {
try {
$stmt = $db->prepare("DELETE FROM appointments WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(['success' => true]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'error' => 'DB Error: ' . $e->getMessage()]);
}
} else {
echo json_encode(['success' => false, 'error' => 'Missing ID']);
}
} else {
echo json_encode(['success' => false, 'error' => 'Invalid action']);
}
exit;
}

274
api/billing.php Normal file
View File

@ -0,0 +1,274 @@
<?php
require_once __DIR__ . '/../db/config.php';
header('Content-Type: application/json');
$db = db();
$action = $_POST['action'] ?? $_GET['action'] ?? '';
if ($action === 'get_bill_details') {
$visit_id = $_GET['visit_id'] ?? 0;
$bill_id_param = $_GET['bill_id'] ?? 0;
if (!$visit_id && !$bill_id_param) {
echo json_encode(['success' => false, 'error' => 'Visit ID or Bill ID required']);
exit;
}
try {
if ($bill_id_param) {
$stmt = $db->prepare("SELECT visit_id FROM bills WHERE id = ?");
$stmt->execute([$bill_id_param]);
$visit_id = $stmt->fetchColumn();
if (!$visit_id) {
echo json_encode(['success' => false, 'error' => 'Bill not found']);
exit;
}
}
// 1. Get Visit & Patient Info
$stmt = $db->prepare("
SELECT v.*, p.name as patient_name, p.insurance_company_id, ic.name_en as insurance_name
FROM visits v
JOIN patients p ON v.patient_id = p.id
LEFT JOIN insurance_companies ic ON p.insurance_company_id = ic.id
WHERE v.id = ?
");
$stmt->execute([$visit_id]);
$visit = $stmt->fetch();
if (!$visit) {
echo json_encode(['success' => false, 'error' => 'Visit not found']);
exit;
}
// 2. Get or Create Bill
$stmt = $db->prepare("SELECT * FROM bills WHERE visit_id = ?");
$stmt->execute([$visit_id]);
$bill = $stmt->fetch();
if (!$bill) {
$stmt = $db->prepare("INSERT INTO bills (patient_id, visit_id, status, created_at) VALUES (?, ?, 'Pending', NOW())");
$stmt->execute([$visit['patient_id'], $visit_id]);
$bill_id = $db->lastInsertId();
// Re-fetch
$stmt = $db->prepare("SELECT * FROM bills WHERE id = ?");
$stmt->execute([$bill_id]);
$bill = $stmt->fetch();
} else {
$bill_id = $bill['id'];
}
// --- AUTO-ADD ITEMS FROM OTHER DEPARTMENTS ---
// Fetch existing items to prevent duplicates
$stmt = $db->prepare("SELECT description FROM bill_items WHERE bill_id = ?");
$stmt->execute([$bill_id]);
$existing_items = $stmt->fetchAll(PDO::FETCH_COLUMN);
// Helper to check and add
$added_count = 0;
// 1. X-Rays
$stmt = $db->prepare("
SELECT xt.name_en, xt.price
FROM xray_inquiry_items xii
JOIN xray_inquiries xi ON xii.inquiry_id = xi.id
JOIN xray_tests xt ON xii.xray_id = xt.id
WHERE xi.visit_id = ?
");
$stmt->execute([$visit_id]);
$xrays = $stmt->fetchAll();
foreach ($xrays as $x) {
$desc = "X-Ray: " . $x['name_en'];
if (!in_array($desc, $existing_items)) {
$stmt = $db->prepare("INSERT INTO bill_items (bill_id, description, amount) VALUES (?, ?, ?)");
$stmt->execute([$bill_id, $desc, $x['price']]);
$existing_items[] = $desc; // Update local cache
$added_count++;
}
}
// 2. Labs
$stmt = $db->prepare("
SELECT lt.name_en, lt.price
FROM inquiry_tests it
JOIN laboratory_inquiries li ON it.inquiry_id = li.id
JOIN laboratory_tests lt ON it.test_id = lt.id
WHERE li.visit_id = ?
");
$stmt->execute([$visit_id]);
$labs = $stmt->fetchAll();
foreach ($labs as $l) {
$desc = "Lab: " . $l['name_en'];
if (!in_array($desc, $existing_items)) {
$stmt = $db->prepare("INSERT INTO bill_items (bill_id, description, amount) VALUES (?, ?, ?)");
$stmt->execute([$bill_id, $desc, $l['price']]);
$existing_items[] = $desc;
$added_count++;
}
}
// 3. Drugs (Prescriptions)
$stmt = $db->prepare("
SELECT vp.drug_name, d.price
FROM visit_prescriptions vp
LEFT JOIN drugs d ON (vp.drug_name = d.name_en OR vp.drug_name = d.name_ar)
WHERE vp.visit_id = ?
");
$stmt->execute([$visit_id]);
$drugs = $stmt->fetchAll();
foreach ($drugs as $d) {
$price = $d['price'] ?: 0; // Default to 0 if not found
$desc = "Pharmacy: " . $d['drug_name'];
if (!in_array($desc, $existing_items)) {
$stmt = $db->prepare("INSERT INTO bill_items (bill_id, description, amount) VALUES (?, ?, ?)");
$stmt->execute([$bill_id, $desc, $price]);
$existing_items[] = $desc;
$added_count++;
}
}
// If items were added, update the bill total
if ($added_count > 0) {
updateBillTotal($db, $bill_id);
// Re-fetch bill to get updated total if needed (though calculate total below handles it)
$stmt = $db->prepare("SELECT * FROM bills WHERE id = ?");
$stmt->execute([$bill_id]);
$bill = $stmt->fetch();
}
// ---------------------------------------------
// 3. Get Bill Items (Fresh)
$stmt = $db->prepare("SELECT * FROM bill_items WHERE bill_id = ?");
$stmt->execute([$bill['id']]);
$items = $stmt->fetchAll();
// 4. Calculate Totals (if not synced)
$total = 0;
foreach ($items as $item) {
$total += $item['amount'];
}
// Return Data
echo json_encode([
'success' => true,
'visit' => $visit,
'bill' => $bill,
'items' => $items,
'calculated_total' => $total,
'has_insurance' => !empty($visit['insurance_company_id']),
'insurance_name' => $visit['insurance_name']
]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
}
elseif ($action === 'add_item') {
$bill_id = $_POST['bill_id'] ?? 0;
$description = $_POST['description'] ?? '';
$amount = $_POST['amount'] ?? 0;
if (!$bill_id || !$description || !$amount) {
echo json_encode(['success' => false, 'error' => 'Missing fields']);
exit;
}
try {
$stmt = $db->prepare("INSERT INTO bill_items (bill_id, description, amount) VALUES (?, ?, ?)");
$stmt->execute([$bill_id, $description, $amount]);
// Update Bill Total
updateBillTotal($db, $bill_id);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
}
elseif ($action === 'remove_item') {
$item_id = $_POST['item_id'] ?? 0;
if (!$item_id) {
echo json_encode(['success' => false, 'error' => 'Item ID required']);
exit;
}
try {
// Get bill_id first
$stmt = $db->prepare("SELECT bill_id FROM bill_items WHERE id = ?");
$stmt->execute([$item_id]);
$row = $stmt->fetch();
if ($row) {
$stmt = $db->prepare("DELETE FROM bill_items WHERE id = ?");
$stmt->execute([$item_id]);
updateBillTotal($db, $row['bill_id']);
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
}
elseif ($action === 'update_totals') {
$bill_id = $_POST['bill_id'] ?? 0;
$insurance_covered = $_POST['insurance_covered'] ?? 0;
$patient_payable = $_POST['patient_payable'] ?? 0;
try {
$stmt = $db->prepare("UPDATE bills SET insurance_covered = ?, patient_payable = ? WHERE id = ?");
$stmt->execute([$insurance_covered, $patient_payable, $bill_id]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
}
elseif ($action === 'complete_payment') {
$bill_id = $_POST['bill_id'] ?? 0;
$payment_method = $_POST['payment_method'] ?? 'Cash';
$notes = $_POST['notes'] ?? '';
try {
$db->beginTransaction();
// Update Bill
$stmt = $db->prepare("UPDATE bills SET status = 'Paid', payment_method = ?, notes = ? WHERE id = ?");
$stmt->execute([$payment_method, $notes, $bill_id]);
// Get Visit ID
$stmt = $db->prepare("SELECT visit_id FROM bills WHERE id = ?");
$stmt->execute([$bill_id]);
$bill = $stmt->fetch();
// Update Visit
if ($bill && $bill['visit_id']) {
$stmt = $db->prepare("UPDATE visits SET status = 'Completed', checkout_time = NOW() WHERE id = ?");
$stmt->execute([$bill['visit_id']]);
}
$db->commit();
echo json_encode(['success' => true]);
} catch (Exception $e) {
$db->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
}
function updateBillTotal($db, $bill_id) {
$stmt = $db->prepare("SELECT SUM(amount) FROM bill_items WHERE bill_id = ?");
$stmt->execute([$bill_id]);
$total = $stmt->fetchColumn() ?: 0;
$stmt = $db->prepare("UPDATE bills SET total_amount = ? WHERE id = ?");
$stmt->execute([$total, $bill_id]);
}

90
api/biometric_push.php Normal file
View File

@ -0,0 +1,90 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
try {
$pdo = db();
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Database connection failed']);
exit;
}
// Read input
$input = file_get_contents('php://input');
$data = json_decode($input, true);
if (!$data) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Invalid JSON']);
exit;
}
// Basic Auth (API Key)
// In production, check against biometric_devices table
$api_key = $data['api_key'] ?? '';
if ($api_key !== 'test_key') {
// Check DB
$stmt = $pdo->prepare("SELECT id FROM biometric_devices WHERE api_key = ? AND status = 1");
$stmt->execute([$api_key]);
if (!$stmt->fetch()) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Invalid API Key']);
exit;
}
}
// Validate Data
$employee_id = $data['employee_id'] ?? null;
$timestamp = $data['timestamp'] ?? date('Y-m-d H:i:s'); // ISO 8601 or Y-m-d H:i:s
$type = $data['type'] ?? 'check_in'; // check_in or check_out
if (!$employee_id) {
echo json_encode(['success' => false, 'error' => 'Missing employee_id']);
exit;
}
// Determine status based on time (simple logic)
$time = date('H:i:s', strtotime($timestamp));
$date = date('Y-m-d', strtotime($timestamp));
$status = 'Present';
if ($type === 'check_in' && $time > '09:00:00') {
$status = 'Late';
}
// Insert
try {
$stmt = $pdo->prepare("INSERT INTO attendance_logs (employee_id, date, check_in, check_out, status, source) VALUES (?, ?, ?, ?, ?, 'Biometric Device')");
$check_in = ($type === 'check_in') ? date('Y-m-d H:i:s', strtotime($timestamp)) : null;
$check_out = ($type === 'check_out') ? date('Y-m-d H:i:s', strtotime($timestamp)) : null;
// Check if entry exists for this date to update instead of insert?
// For simplicity, we just insert logs. A real system might merge them.
// Let's try to find an existing log for today
$existing = $pdo->prepare("SELECT id FROM attendance_logs WHERE employee_id = ? AND date = ? ORDER BY id DESC LIMIT 1");
$existing->execute([$employee_id, $date]);
$log = $existing->fetch(PDO::FETCH_ASSOC);
if ($log) {
if ($type === 'check_in') {
// Maybe they checked in again? Update check_in if null
$upd = $pdo->prepare("UPDATE attendance_logs SET check_in = ? WHERE id = ? AND check_in IS NULL");
$upd->execute([$check_in, $log['id']]);
} else {
// Check out
$upd = $pdo->prepare("UPDATE attendance_logs SET check_out = ? WHERE id = ?");
$upd->execute([$check_out, $log['id']]);
}
$msg = "Updated existing log";
} else {
$stmt->execute([$employee_id, $date, $check_in, $check_out, $status]);
$msg = "Created new log";
}
echo json_encode(['success' => true, 'message' => $msg]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

36
api/cities.php Normal file
View File

@ -0,0 +1,36 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../helpers.php';
header('Content-Type: application/json');
$db = db();
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$action = $input['action'] ?? '';
if ($action === 'add_city') {
$name_en = $input['name_en'] ?? '';
$name_ar = $input['name_ar'] ?? '';
if ($name_en && $name_ar) {
try {
$stmt = $db->prepare("INSERT INTO cities (name_en, name_ar) VALUES (?, ?)");
$stmt->execute([$name_en, $name_ar]);
$id = $db->lastInsertId();
echo json_encode(['success' => true, 'id' => $id, 'name_en' => $name_en, 'name_ar' => $name_ar]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
} else {
echo json_encode(['success' => false, 'error' => 'Missing fields']);
}
} else {
echo json_encode(['success' => false, 'error' => 'Invalid action']);
}
exit;
}
echo json_encode(['success' => false, 'error' => 'Invalid request method']);

32
api/doctor_holidays.php Normal file
View File

@ -0,0 +1,32 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../helpers.php';
// Prevent caching
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
header('Content-Type: application/json');
$db = db();
$doctor_id = $_GET['doctor_id'] ?? null;
if (!$doctor_id) {
echo json_encode(['success' => false, 'error' => 'Missing doctor_id']);
exit;
}
try {
// $doctor_id is expected to be employee_id now
$stmt = $db->prepare("
SELECT start_date, end_date, reason
FROM leave_requests
WHERE employee_id = ? AND status = 'Approved' AND end_date >= CURDATE()
");
$stmt->execute([$doctor_id]);
$holidays = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'holidays' => $holidays]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'error' => 'DB Error: ' . $e->getMessage()]);
}

40
api/inventory.php Normal file
View File

@ -0,0 +1,40 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../includes/auth.php';
header('Content-Type: application/json');
// Check if user is logged in
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
// Check permission
if (!has_permission('inventory')) {
http_response_code(403);
echo json_encode(['error' => 'Forbidden: Stock Management permission required']);
exit;
}
$action = $_GET['action'] ?? '';
$db = db();
if ($action === 'get_batches') {
$item_id = $_GET['item_id'] ?? 0;
if (!$item_id) {
echo json_encode([]);
exit;
}
$stmt = $db->prepare("SELECT id, batch_number, quantity, expiry_date, cost_price FROM inventory_batches WHERE item_id = ? AND quantity > 0 ORDER BY expiry_date ASC");
$stmt->execute([$item_id]);
$batches = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($batches);
exit;
}
echo json_encode(['error' => 'Invalid action']);

51
api/patients.php Normal file
View File

@ -0,0 +1,51 @@
<?php
require_once __DIR__ . '/../db/config.php';
header('Content-Type: application/json');
$action = $_GET['action'] ?? '';
$pdo = db();
try {
switch ($action) {
case 'search':
$q = $_GET['q'] ?? '';
if (empty($q)) {
echo json_encode(['results' => []]);
exit;
}
// Search by name, phone or id (patient number)
$sql = "SELECT id, name, phone, civil_id FROM patients WHERE name LIKE ? OR phone LIKE ? OR civil_id LIKE ? OR id = ? LIMIT 20";
$stmt = $pdo->prepare($sql);
$term = "%$q%";
$id_term = intval($q); // for exact match on patient number
$stmt->execute([$term, $term, $term, $id_term]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Format results for select2
$formatted_results = array_map(function($p) {
$patient_number = sprintf('%06d', $p['id']);
$display_text = $patient_number . ' - ' . $p['name'];
if (!empty($p['phone'])) {
$display_text .= ' - ' . $p['phone'];
}
return [
'id' => $p['id'],
'text' => $display_text,
'name' => $p['name'],
'phone' => $p['phone']
];
}, $results);
echo json_encode(['results' => $formatted_results]);
break;
default:
http_response_code(400);
echo json_encode(['error' => 'Invalid action']);
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}

483
api/pharmacy.php Normal file
View File

@ -0,0 +1,483 @@
<?php
require_once __DIR__ . '/../db/config.php';
header('Content-Type: application/json');
$action = $_GET['action'] ?? '';
$pdo = db();
try {
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10;
$offset = ($page - 1) * $limit;
switch ($action) {
case 'get_stock':
// List all drugs with total stock quantity
$sql = "SELECT d.id, d.name_en, d.name_ar, d.min_stock_level, d.reorder_level, d.unit,
COALESCE(SUM(b.quantity), 0) as total_stock
FROM drugs d
LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE()
GROUP BY d.id
ORDER BY d.name_en ASC";
$stmt = $pdo->query($sql);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
break;
case 'get_low_stock':
// Count total for pagination
$countSql = "SELECT COUNT(*) FROM (
SELECT d.id
FROM drugs d
LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE()
GROUP BY d.id
HAVING COALESCE(SUM(b.quantity), 0) <= MAX(d.reorder_level)
) as total";
$total = $pdo->query($countSql)->fetchColumn();
// List drugs where total stock is <= reorder_level
$sql = "SELECT d.id, d.name_en, d.name_ar, d.min_stock_level, d.reorder_level, d.unit,
COALESCE(SUM(b.quantity), 0) as total_stock
FROM drugs d
LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE()
GROUP BY d.id
HAVING total_stock <= MAX(d.reorder_level)
ORDER BY total_stock ASC
LIMIT :limit OFFSET :offset";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit
]);
break;
case 'get_expired':
// Count total
$countSql = "SELECT COUNT(*)
FROM pharmacy_batches b
JOIN drugs d ON b.drug_id = d.id
LEFT JOIN suppliers s ON b.supplier_id = s.id
WHERE b.expiry_date < CURDATE() AND b.quantity > 0";
$total = $pdo->query($countSql)->fetchColumn();
// List batches that have expired and still have quantity
$sql = "SELECT b.id, b.batch_number, b.expiry_date, b.quantity,
d.name_en as drug_name, d.name_ar as drug_name_ar,
s.name_en as supplier_name
FROM pharmacy_batches b
JOIN drugs d ON b.drug_id = d.id
LEFT JOIN suppliers s ON b.supplier_id = s.id
WHERE b.expiry_date < CURDATE() AND b.quantity > 0
ORDER BY b.expiry_date ASC
LIMIT :limit OFFSET :offset";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit
]);
break;
case 'get_near_expiry':
$days = $_GET['days'] ?? 90;
// Count total
$countSql = "SELECT COUNT(*)
FROM pharmacy_batches b
JOIN drugs d ON b.drug_id = d.id
LEFT JOIN suppliers s ON b.supplier_id = s.id
WHERE b.expiry_date >= CURDATE()
AND b.expiry_date <= DATE_ADD(CURDATE(), INTERVAL ? DAY)
AND b.quantity > 0";
$countStmt = $pdo->prepare($countSql);
$countStmt->execute([$days]);
$total = $countStmt->fetchColumn();
// List batches expiring in the next X days
$sql = "SELECT b.id, b.batch_number, b.expiry_date, b.quantity,
d.name_en as drug_name, d.name_ar as drug_name_ar,
s.name_en as supplier_name,
DATEDIFF(b.expiry_date, CURDATE()) as days_remaining
FROM pharmacy_batches b
JOIN drugs d ON b.drug_id = d.id
LEFT JOIN suppliers s ON b.supplier_id = s.id
WHERE b.expiry_date >= CURDATE()
AND b.expiry_date <= DATE_ADD(CURDATE(), INTERVAL :days DAY)
AND b.quantity > 0
ORDER BY b.expiry_date ASC
LIMIT :limit OFFSET :offset";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':days', $days, PDO::PARAM_INT);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit
]);
break;
case 'get_batches':
$drug_id = $_GET['drug_id'] ?? 0;
if (!$drug_id) throw new Exception("Drug ID required");
$sql = "SELECT b.*, s.name_en as supplier_name
FROM pharmacy_batches b
LEFT JOIN suppliers s ON b.supplier_id = s.id
WHERE b.drug_id = ? AND b.quantity > 0
ORDER BY b.expiry_date ASC";
$stmt = $pdo->prepare($sql);
$stmt->execute([$drug_id]);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
break;
case 'add_stock':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Invalid method");
$drug_id = $_POST['drug_id'] ?? 0;
$batch_number = $_POST['batch_number'] ?? '';
$expiry_date = $_POST['expiry_date'] ?? '';
$quantity = $_POST['quantity'] ?? 0;
$cost_price = $_POST['cost_price'] ?? 0;
$sale_price = $_POST['sale_price'] ?? 0;
$supplier_id = !empty($_POST['supplier_id']) ? $_POST['supplier_id'] : null;
if (!$drug_id || !$batch_number || !$expiry_date || !$quantity) {
throw new Exception("Missing required fields");
}
$stmt = $pdo->prepare("INSERT INTO pharmacy_batches
(drug_id, batch_number, expiry_date, quantity, cost_price, sale_price, supplier_id, received_date)
VALUES (?, ?, ?, ?, ?, ?, ?, CURDATE())");
$stmt->execute([$drug_id, $batch_number, $expiry_date, $quantity, $cost_price, $sale_price, $supplier_id]);
echo json_encode(['success' => true, 'message' => 'Stock added successfully']);
break;
case 'search_drugs':
$q = $_GET['q'] ?? '';
$sql = "SELECT d.id, d.name_en, d.name_ar, d.sku, d.price as default_price,
(SELECT sale_price FROM pharmacy_batches pb WHERE pb.drug_id = d.id AND pb.quantity > 0 AND pb.expiry_date >= CURDATE() ORDER BY pb.expiry_date ASC LIMIT 1) as batch_price,
COALESCE(SUM(b.quantity), 0) as stock
FROM drugs d
LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE()
WHERE (d.name_en LIKE ? OR d.name_ar LIKE ? OR d.sku LIKE ?)
GROUP BY d.id
LIMIT 20";
$stmt = $pdo->prepare($sql);
$term = "%$q%";
$stmt->execute([$term, $term, $term]);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
break;
case 'create_sale':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Invalid method");
$input = json_decode(file_get_contents('php://input'), true);
if (empty($input['items'])) throw new Exception("No items in sale");
// Check Settings
$allow_negative = false;
try {
$settingStmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = 'allow_negative_stock'");
$settingStmt->execute();
$allow_negative = (bool)$settingStmt->fetchColumn();
} catch (Exception $e) { /* ignore */ }
$pdo->beginTransaction();
try {
// Create Sale Record
$stmt = $pdo->prepare("INSERT INTO pharmacy_sales (patient_id, visit_id, total_amount, payment_method, status) VALUES (?, ?, ?, ?, 'completed')");
$stmt->execute([
$input['patient_id'] ?? null,
$input['visit_id'] ?? null,
$input['total_amount'] ?? 0,
$input['payment_method'] ?? 'cash'
]);
$sale_id = $pdo->lastInsertId();
// Process Items
foreach ($input['items'] as $item) {
$drug_id = $item['drug_id'];
$qty_needed = $item['quantity'];
$unit_price = $item['price']; // Or fetch from batch? Use provided price for now.
// Fetch available batches (FIFO)
$batch_stmt = $pdo->prepare("SELECT id, quantity FROM pharmacy_batches WHERE drug_id = ? AND quantity > 0 ORDER BY expiry_date ASC FOR UPDATE");
$batch_stmt->execute([$drug_id]);
$batches = $batch_stmt->fetchAll(PDO::FETCH_ASSOC);
$qty_remaining = $qty_needed;
foreach ($batches as $batch) {
if ($qty_remaining <= 0) break;
$take = min($batch['quantity'], $qty_remaining);
// Deduct from batch
$update = $pdo->prepare("UPDATE pharmacy_batches SET quantity = quantity - ? WHERE id = ?");
$update->execute([$take, $batch['id']]);
// Add to sale items
$item_stmt = $pdo->prepare("INSERT INTO pharmacy_sale_items (sale_id, drug_id, batch_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?, ?)");
$item_stmt->execute([$sale_id, $drug_id, $batch['id'], $take, $unit_price, $take * $unit_price]);
$qty_remaining -= $take;
}
if ($qty_remaining > 0) {
if ($allow_negative) {
// Record sale for remaining quantity without batch
$item_stmt = $pdo->prepare("INSERT INTO pharmacy_sale_items (sale_id, drug_id, batch_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?, ?)");
$item_stmt->execute([$sale_id, $drug_id, null, $qty_remaining, $unit_price, $qty_remaining * $unit_price]);
} else {
throw new Exception("Insufficient stock for drug ID: $drug_id");
}
}
}
$pdo->commit();
echo json_encode(['success' => true, 'sale_id' => $sale_id]);
} catch (Exception $e) {
$pdo->rollBack();
throw $e;
}
break;
case 'get_sales':
// List recent sales
$sql = "SELECT s.*, p.name as patient_name
FROM pharmacy_sales s
LEFT JOIN patients p ON s.patient_id = p.id
ORDER BY s.created_at DESC LIMIT 50";
$stmt = $pdo->query($sql);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
break;
case 'get_sale_details':
$sale_id = $_GET['sale_id'] ?? 0;
if (!$sale_id) throw new Exception("Sale ID required");
$stmt = $pdo->prepare("SELECT s.*, p.name as patient_name FROM pharmacy_sales s LEFT JOIN patients p ON s.patient_id = p.id WHERE s.id = ?");
$stmt->execute([$sale_id]);
$sale = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$sale) throw new Exception("Sale not found");
$items_stmt = $pdo->prepare("SELECT i.*, d.name_en as drug_name
FROM pharmacy_sale_items i
JOIN drugs d ON i.drug_id = d.id
WHERE i.sale_id = ?");
$items_stmt->execute([$sale_id]);
$sale['items'] = $items_stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($sale);
break;
case 'get_report':
$type = $_GET['type'] ?? 'inventory_valuation';
$startDate = $_GET['start_date'] ?? date('Y-m-01');
$endDate = $_GET['end_date'] ?? date('Y-m-d');
if ($type === 'inventory_valuation') {
// Count distinct drugs in stock for pagination
$countSql = "SELECT COUNT(DISTINCT d.id)
FROM drugs d
JOIN pharmacy_batches b ON d.id = b.drug_id
WHERE b.quantity > 0";
$total = $pdo->query($countSql)->fetchColumn();
$sql = "SELECT d.name_en as drug_name, d.name_ar as drug_name_ar,
g.name_en as category_name,
SUM(b.quantity) as stock_quantity,
SUM(b.quantity * b.cost_price) / SUM(b.quantity) as avg_cost,
SUM(b.quantity * b.sale_price) / SUM(b.quantity) as selling_price,
SUM(b.quantity * b.cost_price) as total_cost_value,
SUM(b.quantity * b.sale_price) as total_sales_value
FROM drugs d
JOIN pharmacy_batches b ON d.id = b.drug_id
LEFT JOIN drugs_groups g ON d.group_id = g.id
WHERE b.quantity > 0
GROUP BY d.id
ORDER BY d.name_en ASC
LIMIT :limit OFFSET :offset";
// Calculate Grand Totals (entire stock)
$grandTotalSql = "SELECT SUM(b.quantity * b.cost_price) as total_cost,
SUM(b.quantity * b.sale_price) as total_sales
FROM pharmacy_batches b
WHERE b.quantity > 0";
$grandTotals = $pdo->query($grandTotalSql)->fetch(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit,
'grand_total_cost' => $grandTotals['total_cost'] ?? 0,
'grand_total_sales' => $grandTotals['total_sales'] ?? 0
]);
} elseif ($type === 'sales') {
// Count
$countSql = "SELECT COUNT(*) FROM pharmacy_sales WHERE created_at BETWEEN ? AND ? + INTERVAL 1 DAY";
$countStmt = $pdo->prepare($countSql);
$countStmt->execute([$startDate, $endDate]);
$total = $countStmt->fetchColumn();
$sql = "SELECT s.id, s.created_at, s.total_amount, s.payment_method,
p.name as patient_name,
(SELECT COUNT(*) FROM pharmacy_sale_items i WHERE i.sale_id = s.id) as item_count
FROM pharmacy_sales s
LEFT JOIN patients p ON s.patient_id = p.id
WHERE s.created_at BETWEEN :start AND :end + INTERVAL 1 DAY
ORDER BY s.created_at DESC
LIMIT :limit OFFSET :offset";
$grandTotalSql = "SELECT SUM(total_amount) as total FROM pharmacy_sales WHERE created_at BETWEEN ? AND ? + INTERVAL 1 DAY";
$grandTotalStmt = $pdo->prepare($grandTotalSql);
$grandTotalStmt->execute([$startDate, $endDate]);
$grandTotal = $grandTotalStmt->fetchColumn();
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':start', $startDate);
$stmt->bindValue(':end', $endDate);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit,
'grand_total_sales' => $grandTotal ?? 0
]);
} elseif ($type === 'expiry') {
$countSql = "SELECT COUNT(*) FROM pharmacy_batches WHERE expiry_date BETWEEN ? AND ? AND quantity > 0";
$countStmt = $pdo->prepare($countSql);
$countStmt->execute([$startDate, $endDate]);
$total = $countStmt->fetchColumn();
$sql = "SELECT b.id, b.batch_number, b.expiry_date, b.quantity,
d.name_en as drug_name, d.name_ar as drug_name_ar,
s.name_en as supplier_name,
DATEDIFF(b.expiry_date, CURDATE()) as days_remaining
FROM pharmacy_batches b
JOIN drugs d ON b.drug_id = d.id
LEFT JOIN suppliers s ON b.supplier_id = s.id
WHERE b.expiry_date BETWEEN :start AND :end AND b.quantity > 0
ORDER BY b.expiry_date ASC
LIMIT :limit OFFSET :offset";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':start', $startDate);
$stmt->bindValue(':end', $endDate);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit
]);
} elseif ($type === 'purchase_report') {
$countSql = "SELECT COUNT(*) FROM pharmacy_lpos WHERE lpo_date BETWEEN ? AND ?";
$countStmt = $pdo->prepare($countSql);
$countStmt->execute([$startDate, $endDate]);
$total = $countStmt->fetchColumn();
$sql = "SELECT l.id, l.lpo_date, l.status, l.total_amount, s.name_en as supplier_name, s.name_ar as supplier_name_ar
FROM pharmacy_lpos l
LEFT JOIN suppliers s ON l.supplier_id = s.id
WHERE l.lpo_date BETWEEN :start AND :end
ORDER BY l.lpo_date DESC
LIMIT :limit OFFSET :offset";
$grandTotalSql = "SELECT SUM(total_amount) as total FROM pharmacy_lpos WHERE lpo_date BETWEEN ? AND ?";
$grandTotalStmt = $pdo->prepare($grandTotalSql);
$grandTotalStmt->execute([$startDate, $endDate]);
$grandTotal = $grandTotalStmt->fetchColumn();
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':start', $startDate);
$stmt->bindValue(':end', $endDate);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit,
'grand_total_purchases' => $grandTotal ?? 0
]);
} elseif ($type === 'low_stock') {
// Reuse get_low_stock logic
$countSql = "SELECT COUNT(*) FROM (
SELECT d.id
FROM drugs d
LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE()
GROUP BY d.id
HAVING COALESCE(SUM(b.quantity), 0) <= MAX(d.reorder_level)
) as total";
$total = $pdo->query($countSql)->fetchColumn();
$sql = "SELECT d.id, d.name_en, d.name_ar, d.min_stock_level, d.reorder_level, d.unit,
COALESCE(SUM(b.quantity), 0) as total_stock
FROM drugs d
LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE()
GROUP BY d.id
HAVING total_stock <= MAX(d.reorder_level)
ORDER BY total_stock ASC
LIMIT :limit OFFSET :offset";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit
]);
}
break;
default:
throw new Exception("Invalid action");
}
} catch (Exception $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
}

282
api/pharmacy_lpo.php Normal file
View File

@ -0,0 +1,282 @@
<?php
require_once __DIR__ . '/../db/config.php';
header('Content-Type: application/json');
$action = $_GET['action'] ?? '';
try {
$pdo = db();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($action === 'create_lpo') {
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data['supplier_id']) || empty($data['items'])) {
throw new Exception("Supplier and items are required.");
}
$pdo->beginTransaction();
$stmt = $pdo->prepare("INSERT INTO pharmacy_lpos (supplier_id, lpo_date, status, total_amount, notes) VALUES (?, ?, 'Draft', ?, ?)");
$stmt->execute([
$data['supplier_id'],
$data['lpo_date'] ?? date('Y-m-d'),
$data['total_amount'] ?? 0,
$data['notes'] ?? ''
]);
$lpoId = $pdo->lastInsertId();
$stmtItem = $pdo->prepare("INSERT INTO pharmacy_lpo_items (lpo_id, drug_id, quantity, cost_price, total_cost, batch_number, expiry_date) VALUES (?, ?, ?, ?, ?, ?, ?)");
foreach ($data['items'] as $item) {
$stmtItem->execute([
$lpoId,
$item['drug_id'],
$item['quantity'],
$item['cost_price'],
$item['total_cost'],
!empty($item['batch_number']) ? $item['batch_number'] : null,
!empty($item['expiry_date']) ? $item['expiry_date'] : null
]);
}
$pdo->commit();
echo json_encode(['success' => true, 'message' => 'Purchase created successfully']);
} elseif ($action === 'update_status') {
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data['id']) || empty($data['status'])) {
throw new Exception("ID and Status are required");
}
$pdo->beginTransaction();
// Check if items update is requested (specifically for Received status or corrections)
if (!empty($data['items']) && is_array($data['items'])) {
$updateItemStmt = $pdo->prepare("UPDATE pharmacy_lpo_items SET batch_number = ?, expiry_date = ? WHERE id = ?");
foreach ($data['items'] as $item) {
if (!empty($item['id'])) {
$updateItemStmt->execute([
!empty($item['batch_number']) ? $item['batch_number'] : null,
!empty($item['expiry_date']) ? $item['expiry_date'] : null,
$item['id']
]);
}
}
}
// If status is being changed to Received, we must update stock
if ($data['status'] === 'Received') {
// Fetch LPO items (re-fetch to get updated values)
$stmtItems = $pdo->prepare("SELECT * FROM pharmacy_lpo_items WHERE lpo_id = ?");
$stmtItems->execute([$data['id']]);
$items = $stmtItems->fetchAll(PDO::FETCH_ASSOC);
// Fetch LPO details for supplier
$stmtLPO = $pdo->prepare("SELECT supplier_id FROM pharmacy_lpos WHERE id = ?");
$stmtLPO->execute([$data['id']]);
$lpo = $stmtLPO->fetch(PDO::FETCH_ASSOC);
$batchStmt = $pdo->prepare("INSERT INTO pharmacy_batches (drug_id, batch_number, expiry_date, quantity, cost_price, sale_price, supplier_id, received_date) VALUES (?, ?, ?, ?, ?, ?, ?, CURDATE())");
// We need sale price for the batch. Ideally, LPO should have it or we fetch current from drugs table.
$drugPriceStmt = $pdo->prepare("SELECT price FROM drugs WHERE id = ?");
foreach ($items as $item) {
if (empty($item['batch_number']) || empty($item['expiry_date'])) {
// If still missing (should be caught by UI), generate defaults
$item['batch_number'] = $item['batch_number'] ?? 'BATCH-' . date('Ymd') . '-' . $item['id'];
$item['expiry_date'] = $item['expiry_date'] ?? date('Y-m-d', strtotime('+1 year'));
}
$drugPriceStmt->execute([$item['drug_id']]);
$drug = $drugPriceStmt->fetch(PDO::FETCH_ASSOC);
$salePrice = $drug['price'] ?? ($item['cost_price'] * 1.5); // Fallback margin
$batchStmt->execute([
$item['drug_id'],
$item['batch_number'],
$item['expiry_date'],
$item['quantity'],
$item['cost_price'],
$salePrice,
$lpo['supplier_id']
]);
}
}
$stmt = $pdo->prepare("UPDATE pharmacy_lpos SET status = ? WHERE id = ?");
$stmt->execute([$data['status'], $data['id']]);
$pdo->commit();
echo json_encode(['success' => true]);
} elseif ($action === 'create_return') {
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data['supplier_id']) || empty($data['items'])) {
throw new Exception("Supplier and items are required.");
}
$pdo->beginTransaction();
$stmt = $pdo->prepare("INSERT INTO pharmacy_purchase_returns (supplier_id, return_date, total_amount, reason) VALUES (?, ?, ?, ?)");
$stmt->execute([
$data['supplier_id'],
$data['return_date'] ?? date('Y-m-d'),
$data['total_amount'] ?? 0,
$data['reason'] ?? ''
]);
$returnId = $pdo->lastInsertId();
$stmtItem = $pdo->prepare("INSERT INTO pharmacy_purchase_return_items (return_id, drug_id, batch_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?, ?)");
$updateBatch = $pdo->prepare("UPDATE pharmacy_batches SET quantity = quantity - ? WHERE id = ?");
foreach ($data['items'] as $item) {
// Check stock first
$checkBatch = $pdo->prepare("SELECT quantity FROM pharmacy_batches WHERE id = ?");
$checkBatch->execute([$item['batch_id']]);
$currentStock = $checkBatch->fetchColumn();
if ($currentStock < $item['quantity']) {
throw new Exception("Insufficient stock in batch for drug ID " . $item['drug_id']);
}
$stmtItem->execute([
$returnId,
$item['drug_id'],
$item['batch_id'],
$item['quantity'],
$item['unit_price'],
$item['total_price']
]);
// Deduct stock
$updateBatch->execute([$item['quantity'], $item['batch_id']]);
}
$pdo->commit();
echo json_encode(['success' => true, 'message' => 'Return created successfully']);
}
} elseif ($_SERVER['REQUEST_METHOD'] === 'GET') {
if ($action === 'get_lpos') {
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 20;
$offset = ($page - 1) * $limit;
// Count total
$countStmt = $pdo->query("SELECT COUNT(*) FROM pharmacy_lpos");
$total = $countStmt->fetchColumn();
$stmt = $pdo->prepare("
SELECT l.*, s.name_en as supplier_name
FROM pharmacy_lpos l
LEFT JOIN suppliers s ON l.supplier_id = s.id
ORDER BY l.created_at DESC
LIMIT :limit OFFSET :offset
");
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit,
'pages' => ceil($total / $limit)
]);
} elseif ($action === 'get_lpo_details') {
$id = $_GET['id'] ?? 0;
$stmt = $pdo->prepare("
SELECT i.*, d.name_en as drug_name, d.sku
FROM pharmacy_lpo_items i
LEFT JOIN drugs d ON i.drug_id = d.id
WHERE i.lpo_id = ?
");
$stmt->execute([$id]);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
} elseif ($action === 'get_suppliers') {
$stmt = $pdo->query("SELECT id, name_en, name_ar FROM suppliers ORDER BY name_en ASC");
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
} elseif ($action === 'get_drugs') {
$stmt = $pdo->query("SELECT id, name_en, name_ar, sku, price FROM drugs ORDER BY name_en ASC");
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
} elseif ($action === 'get_returns') {
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 20;
$offset = ($page - 1) * $limit;
$countStmt = $pdo->query("SELECT COUNT(*) FROM pharmacy_purchase_returns");
$total = $countStmt->fetchColumn();
$stmt = $pdo->prepare("
SELECT r.*, s.name_en as supplier_name
FROM pharmacy_purchase_returns r
LEFT JOIN suppliers s ON r.supplier_id = s.id
ORDER BY r.return_date DESC
LIMIT :limit OFFSET :offset
");
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
echo json_encode([
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'total' => $total,
'page' => $page,
'limit' => $limit,
'pages' => ceil($total / $limit)
]);
} elseif ($action === 'get_return_details') {
$id = $_GET['id'] ?? 0;
$stmt = $pdo->prepare("
SELECT i.*, d.name_en as drug_name, d.sku, b.batch_number
FROM pharmacy_purchase_return_items i
LEFT JOIN drugs d ON i.drug_id = d.id
LEFT JOIN pharmacy_batches b ON i.batch_id = b.id
WHERE i.return_id = ?
");
$stmt->execute([$id]);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
} elseif ($action === 'get_supplier_batches') {
// Get batches for a specific supplier (or all if not specified, but usually filtered by drug)
// Ideally: We select a supplier for return, then we select items.
// Better: Select Drug -> Show Batches (maybe filter by supplier if we track supplier_id in batch)
$drug_id = $_GET['drug_id'] ?? 0;
$supplier_id = $_GET['supplier_id'] ?? 0;
$sql = "SELECT b.id, b.batch_number, b.expiry_date, b.quantity, b.cost_price
FROM pharmacy_batches b
WHERE b.drug_id = ? AND b.quantity > 0";
$params = [$drug_id];
if ($supplier_id) {
// If we want to strictly return only what we bought from this supplier:
// $sql .= " AND b.supplier_id = ?";
// $params[] = $supplier_id;
// BUT, sometimes we might return to a supplier what we bought elsewhere if they accept it,
// or `supplier_id` in batches might be null for old data.
// Let's NOT strictly enforce supplier_id match for now, just show all batches.
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
}
}
} catch (Exception $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}

251
api/queue.php Normal file
View File

@ -0,0 +1,251 @@
<?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.*, td.name_$lang as target_department_name,
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,
d.room_number,
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 departments td ON q.target_department_id = td.id
LEFT JOIN employees 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;
}
// --- TRANSFER TOKEN ---
if ($action === 'transfer') {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new Exception('Invalid request method');
}
$queue_id = $_POST['queue_id'] ?? null;
$new_department_id = $_POST['department_id'] ?? null;
$new_doctor_id = $_POST['doctor_id'] ?? null;
if (!$queue_id || !$new_department_id) {
throw new Exception('Queue ID and Target Department are required');
}
// Get current queue token
$stmt = $db->prepare("SELECT patient_id FROM patient_queue WHERE id = ?");
$stmt->execute([$queue_id]);
$current = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$current) {
throw new Exception('Queue token not found');
}
// Complete the old token
$stmt = $db->prepare("UPDATE patient_queue SET status = 'completed', updated_at = NOW() WHERE id = ?");
$stmt->execute([$queue_id]);
// Create new token
$today = date('Y-m-d');
$stmt = $db->prepare("
SELECT MAX(token_number)
FROM patient_queue
WHERE department_id = ?
AND DATE(created_at) = ?
");
$stmt->execute([$new_department_id, $today]);
$max_token = $stmt->fetchColumn();
$next_token = ($max_token) ? $max_token + 1 : 1;
$stmt = $db->prepare("
INSERT INTO patient_queue (patient_id, department_id, doctor_id, token_number, status, created_at)
VALUES (?, ?, ?, ?, 'waiting', NOW())
");
$stmt->execute([$current['patient_id'], $new_department_id, $new_doctor_id ?: null, $next_token]);
$new_queue_id = $db->lastInsertId();
echo json_encode(['success' => true, 'message' => 'Token transferred', 'token_number' => $next_token, 'new_queue_id' => $new_queue_id]);
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
LEFT JOIN departments td ON q.target_department_id = td.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;
}
// --- GET ADS ---
if ($action === 'get_ads') {
$stmt = $db->query("SELECT * FROM queue_ads WHERE active = 1 ORDER BY created_at DESC");
$ads = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Return both languages
$data = array_map(function($ad) {
return [
'id' => $ad['id'],
'text_en' => $ad['text_en'],
'text_ar' => $ad['text_ar']
];
}, $ads);
echo json_encode(['success' => true, 'data' => $data]);
exit;
}
throw new Exception('Invalid action');
} catch (Exception $e) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

46
apply_migrations.php Normal file
View File

@ -0,0 +1,46 @@
<?php
require_once 'db/config.php';
$db = db();
// Ensure buffered query is on if possible (though config might override)
$db->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
$db->query("SET FOREIGN_KEY_CHECKS=0;");
$files = glob('db/migrations/*.sql');
sort($files);
foreach ($files as $file) {
echo "Processing $file...\n";
$sql_content = file_get_contents($file);
$sql_content = preg_replace('/--.*$/m', '', $sql_content);
$statements = explode(';', $sql_content);
foreach ($statements as $sql) {
$sql = trim($sql);
if (empty($sql)) continue;
try {
// Use query() instead of exec() to handle potential result sets (like SELECT 1)
// and close the cursor explicitly.
$stmt = $db->query($sql);
if ($stmt) {
$stmt->closeCursor();
}
echo "Executed: " . substr(str_replace("\n", " ", $sql), 0, 60) . "...\n";
} catch (PDOException $e) {
$msg = $e->getMessage();
if (strpos($msg, "Duplicate column") !== false ||
strpos($msg, "already exists") !== false ||
strpos($msg, "Duplicate key") !== false ||
strpos($msg, "1062 Duplicate entry") !== false ||
strpos($msg, "1054 Unknown column") !== false ||
strpos($msg, "1146 Table") !== false ||
strpos($msg, "1553 Cannot drop index") !== false || strpos($msg, "1826 Duplicate FOREIGN KEY") !== false || strpos($msg, "1828 Cannot drop column") !== false) {
echo "Skipped (Exists): " . substr(str_replace("\n", " ", $sql), 0, 60) . "...\n";
} else {
echo "Error: " . $msg . "\n";
}
}
}
}
$db->query("SET FOREIGN_KEY_CHECKS=1;");
echo "All migrations applied.\n";

15
appointments.php Normal file
View File

@ -0,0 +1,15 @@
<?php
$section = 'appointments';
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/includes/auth.php';
check_auth();
$db = db();
$lang = $_SESSION['lang'] ?? 'en';
require_once __DIR__ . '/includes/actions.php';
require_once __DIR__ . '/includes/common_data.php';
require_once __DIR__ . '/includes/layout/header.php';
require_once __DIR__ . '/includes/pages/appointments.php';
require_once __DIR__ . '/includes/layout/footer.php';

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

83
assets/js/ai_helper.js Normal file
View File

@ -0,0 +1,83 @@
async function generateAISuggestion(button) {
const target = button.dataset.target || 'treatment_plan';
const modal = button.closest(".modal");
// Get editor instances or DOM elements
const symptomsEditor = $(modal.querySelector('textarea[name="symptoms"]'));
const diagnosisEditor = $(modal.querySelector('textarea[name="diagnosis"]'));
const treatmentPlanEditor = $(modal.querySelector('textarea[name="treatment_plan"]'));
// Helper to get value from Summernote or textarea
const getValue = (editor, selector) => {
if (editor.length && editor.summernote) return editor.summernote('code');
const el = modal.querySelector(selector);
return el ? el.value : '';
};
const symptomsText = getValue(symptomsEditor, 'textarea[name="symptoms"]');
const diagnosisText = getValue(diagnosisEditor, 'textarea[name="diagnosis"]');
// Helper to set value
const setValue = (editor, selector, val) => {
if (editor.length && editor.summernote) {
editor.summernote('code', val);
} else {
const el = modal.querySelector(selector);
if (el) el.value = val;
}
};
// Validation
const cleanSymptoms = symptomsText.replace(/<[^>]*>/g, "").trim();
const cleanDiagnosis = diagnosisText.replace(/<[^>]*>/g, "").trim();
if (target === 'diagnosis' && !cleanSymptoms) {
alert("Please enter symptoms first.");
return;
}
if (target === 'treatment_plan' && (!cleanSymptoms && !cleanDiagnosis)) {
alert("Please enter symptoms or diagnosis first.");
return;
}
const originalHTML = button.innerHTML;
button.disabled = true;
button.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> AI...';
try {
const payload = {
target: target,
symptoms: symptomsText,
diagnosis: diagnosisText,
current_value: (target === 'symptoms' ? symptomsText : (target === 'diagnosis' ? diagnosisText : ''))
};
const response = await fetch("api/ai_report.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const data = await response.json();
if (data.success) {
if (target === 'symptoms') {
setValue(symptomsEditor, 'textarea[name="symptoms"]', data.report);
} else if (target === 'diagnosis') {
setValue(diagnosisEditor, 'textarea[name="diagnosis"]', data.report);
} else {
setValue(treatmentPlanEditor, 'textarea[name="treatment_plan"]', data.report);
}
} else {
alert("AI Error: " + (data.error || "Unknown error"));
}
} catch (error) {
console.error("AI Suggestion failed:", error);
alert("Failed to generate AI suggestion.");
} finally {
button.disabled = false;
button.innerHTML = originalHTML;
}
}
// Alias for backward compatibility
window.generateAIReport = generateAISuggestion;

View File

@ -1,8 +1,10 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// --- Chat Widget Logic ---
const chatForm = document.getElementById('chat-form'); const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input'); const chatInput = document.getElementById('chat-input');
const chatMessages = document.getElementById('chat-messages'); const chatMessages = document.getElementById('chat-messages');
if (chatForm && chatInput && chatMessages) {
const appendMessage = (text, sender) => { const appendMessage = (text, sender) => {
const msgDiv = document.createElement('div'); const msgDiv = document.createElement('div');
msgDiv.classList.add('message', sender); msgDiv.classList.add('message', sender);
@ -36,4 +38,30 @@ document.addEventListener('DOMContentLoaded', () => {
appendMessage("Sorry, something went wrong. Please try again.", 'bot'); appendMessage("Sorry, something went wrong. Please try again.", 'bot');
} }
}); });
}
// --- Patient Form: Auto-calculate DOB from Age ---
// Use jQuery for better compatibility with Inputmask and existing events
function setupAgeToDob(ageId, dobId) {
$(document).on('input', '#' + ageId, function() {
var age = parseInt($(this).val());
var $dob = $('#' + dobId);
if (!isNaN(age) && age >= 0) {
var currentYear = new Date().getFullYear();
var birthYear = currentYear - age;
// Default to Jan 1st of the birth year: YYYY-01-01
var dob = birthYear + '-01-01';
// Set value and trigger input/change for Inputmask and other listeners
$dob.val(dob).trigger('input').trigger('change');
} else {
// Optional: Clear DOB if age is invalid/cleared?
// $dob.val('').trigger('input');
}
});
}
setupAgeToDob('add_patient_age', 'add_patient_dob');
setupAgeToDob('edit_patient_age', 'edit_patient_dob');
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

16
billing.php Normal file
View File

@ -0,0 +1,16 @@
<?php
$section = 'billing';
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/includes/auth.php';
check_auth();
$db = db();
$lang = $_SESSION['lang'];
require_once __DIR__ . '/includes/actions.php';
require_once __DIR__ . '/includes/common_data.php';
require_once __DIR__ . '/includes/layout/header.php';
require_once __DIR__ . '/includes/pages/billing.php';
require_once __DIR__ . '/includes/layout/footer.php';

View File

@ -0,0 +1,6 @@
<?php
require_once 'db/config.php';
$db = db();
$stmt = $db->query("DESCRIBE appointments");
$columns = $stmt->fetchAll(PDO::FETCH_COLUMN);
print_r($columns);

20
check_data.php Normal file
View File

@ -0,0 +1,20 @@
<?php
require_once 'db/config.php';
$db = db();
// Fetch one patient
$patient = $db->query("SELECT id, name FROM patients LIMIT 1")->fetch(PDO::FETCH_ASSOC);
// Fetch one doctor (employee with position 'Doctor')
$doctor = $db->query("
SELECT e.id, e.name_en
FROM employees e
JOIN positions p ON e.position_id = p.id
WHERE UPPER(p.name_en) = 'DOCTOR'
LIMIT 1")->fetch(PDO::FETCH_ASSOC);
if ($patient && $doctor) {
echo "Found Patient: " . $patient['name'] . " (ID: " . $patient['id'] . ")\n";
echo "Found Doctor: " . $doctor['name_en'] . " (ID: " . $doctor['id'] . ")\n";
} else {
echo "Could not find patient or doctor.\n";
}

16
check_details_schema.php Normal file
View File

@ -0,0 +1,16 @@
<?php
require_once 'db/config.php';
$db = db();
$tables = ['xray_inquiry_items', 'inquiry_tests', 'visit_prescriptions', 'laboratory_inquiries'];
foreach ($tables as $table) {
try {
$stmt = $db->query("SHOW CREATE TABLE $table");
$row = $stmt->fetch(PDO::FETCH_ASSOC);
echo $row['Create Table'] . "\n\n";
} catch (Exception $e) {
echo "Table $table not found: " . $e->getMessage() . "\n\n";
}
}

View File

@ -0,0 +1,19 @@
<?php
require_once __DIR__ . '/db/config.php';
$db = db();
try {
$result = $db->query("SHOW COLUMNS FROM doctor_holidays");
if ($result) {
$columns = $result->fetchAll(PDO::FETCH_ASSOC);
echo "Table 'doctor_holidays' exists with columns:\n";
foreach ($columns as $col) {
echo "- " . $col['Field'] . " (" . $col['Type'] . ")\n";
}
} else {
echo "Table 'doctor_holidays' does not exist.\n";
}
} catch (PDOException $e) {
echo "Error: " . $e->getMessage() . "\n";
}

View File

@ -0,0 +1,8 @@
<?php
require_once 'db/config.php';
$db = db();
$stmt = $db->query("DESCRIBE employees");
$columns = $stmt->fetchAll(PDO::FETCH_COLUMN);
echo "Columns in employees table:\n";
print_r($columns);
?>

6
check_nurses_schema.php Normal file
View File

@ -0,0 +1,6 @@
<?php
require_once 'db/config.php';
$db = db();
$stmt = $db->query("DESCRIBE nurses");
$columns = $stmt->fetchAll(PDO::FETCH_COLUMN);
print_r($columns);

24
check_pharmacy_schema.php Normal file
View File

@ -0,0 +1,24 @@
<?php
require_once 'db/config.php';
$pdo = db();
$tables = ['drugs', 'suppliers', 'visit_prescriptions'];
foreach ($tables as $table) {
echo "--- Table: $table ---
";
try {
$stmt = $pdo->query("DESCRIBE $table");
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($columns as $col) {
echo "{$col['Field']} ({$col['Type']})
";
}
} catch (PDOException $e) {
echo "Error describing $table: " . $e->getMessage() . "
";
}
echo "
";
}

16
check_prices_schema.php Normal file
View File

@ -0,0 +1,16 @@
<?php
require_once 'db/config.php';
$db = db();
$tables = ['laboratory_tests', 'xray_tests', 'drugs'];
foreach ($tables as $table) {
try {
$stmt = $db->query("SHOW CREATE TABLE $table");
$row = $stmt->fetch(PDO::FETCH_ASSOC);
echo $row['Create Table'] . "\n\n";
} catch (Exception $e) {
echo "Table $table not found or error: " . $e->getMessage() . "\n\n";
}
}

16
check_schema_billing.php Normal file
View File

@ -0,0 +1,16 @@
<?php
require_once 'db/config.php';
$db = db();
$tables = ['services', 'xray_inquiries', 'laboratory_inquiries', 'visit_prescriptions', 'bill_items'];
foreach ($tables as $table) {
try {
$stmt = $db->query("SHOW CREATE TABLE $table");
$row = $stmt->fetch(PDO::FETCH_ASSOC);
echo $row['Create Table'] . "\n\n";
} catch (Exception $e) {
echo "Table $table not found or error: " . $e->getMessage() . "\n\n";
}
}

16
cities.php Normal file
View File

@ -0,0 +1,16 @@
<?php
$section = 'cities';
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/includes/auth.php';
check_auth();
$db = db();
$lang = $_SESSION['lang'] ?? 'en';
require_once __DIR__ . '/includes/actions.php';
require_once __DIR__ . '/includes/common_data.php';
require_once __DIR__ . '/includes/layout/header.php';
require_once __DIR__ . '/includes/pages/cities.php';
require_once __DIR__ . '/includes/layout/footer.php';

42
dashboard.php Normal file
View File

@ -0,0 +1,42 @@
<?php
// Enable detailed error reporting for debugging 500 errors
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$section = 'dashboard';
require_once __DIR__ . '/db/config.php';
// Try to connect to DB first to catch connection errors early
try {
$db = db();
} catch (PDOException $e) {
die("Database Connection Error: " . $e->getMessage());
} catch (Exception $e) {
die("General Error: " . $e->getMessage());
}
// Now include helpers, which can use the existing $db connection
require_once __DIR__ . '/helpers.php';
// Auth Check
require_once __DIR__ . '/includes/auth.php';
check_auth();
// $db is already set above, so no need to call db() again, but it's safe if we do.
// $db = db();
$lang = $_SESSION['lang'];
require_once __DIR__ . '/includes/actions.php';
require_once __DIR__ . '/includes/common_data.php';
if (!isset($_GET['ajax_search'])) {
require_once __DIR__ . '/includes/layout/header.php';
}
require_once __DIR__ . '/includes/pages/dashboard.php';
if (!isset($_GET['ajax_search'])) {
require_once __DIR__ . '/includes/layout/footer.php';
}

View File

@ -1,17 +1,20 @@
<?php <?php
// Generated by setup_mariadb_project.sh — edit as needed. // Generated by setup_mariadb_project.sh — edit as needed.
define('DB_HOST', '127.0.0.1'); if (!defined('DB_HOST')) define('DB_HOST', getenv('DB_HOST') ?: '127.0.0.1');
define('DB_NAME', 'app_38960'); if (!defined('DB_NAME')) define('DB_NAME', getenv('DB_NAME') ?: 'app_38960');
define('DB_USER', 'app_38960'); if (!defined('DB_USER')) define('DB_USER', getenv('DB_USER') ?: 'app_38960');
define('DB_PASS', '36fb441e-8408-4101-afdc-7911dc065e36'); if (!defined('DB_PASS')) define('DB_PASS', getenv('DB_PASS') ?: '36fb441e-8408-4101-afdc-7911dc065e36');
function db() { if (!function_exists('db')) {
function db() {
static $pdo; static $pdo;
if (!$pdo) { if (!$pdo) {
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [ $pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
]); ]);
} }
return $pdo; return $pdo;
}
} }

View File

@ -0,0 +1,30 @@
-- Migration: Add attachment to inquiry_tests
-- This table might have been created by previous agents or manually
CREATE TABLE IF NOT EXISTS inquiry_tests (
id INT AUTO_INCREMENT PRIMARY KEY,
inquiry_id INT,
test_id INT,
result VARCHAR(255),
normal_range VARCHAR(255),
attachment VARCHAR(255),
FOREIGN KEY (inquiry_id) REFERENCES laboratory_inquiries(id) ON DELETE CASCADE,
FOREIGN KEY (test_id) REFERENCES laboratory_tests(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Ensure attachment column exists (in case inquiry_tests already existed without it)
SET @dbname = DATABASE();
SET @tablename = "inquiry_tests";
SET @columnname = "attachment";
SET @preparedStatement = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = @tablename
AND COLUMN_NAME = @columnname
) > 0,
"SELECT 1",
"ALTER TABLE inquiry_tests ADD COLUMN attachment VARCHAR(255) AFTER normal_range"
));
PREPARE stmt FROM @preparedStatement;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@ -0,0 +1,37 @@
-- Create appointments table
CREATE TABLE IF NOT EXISTS appointments (
id INT AUTO_INCREMENT PRIMARY KEY,
patient_id INT NOT NULL,
doctor_id INT NOT NULL,
start_time DATETIME NOT NULL,
end_time DATETIME NOT NULL,
status ENUM('Scheduled', 'Completed', 'Cancelled') DEFAULT 'Scheduled',
reason TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (patient_id) REFERENCES patients(id) ON DELETE CASCADE,
FOREIGN KEY (doctor_id) REFERENCES doctors(id) ON DELETE CASCADE
);
-- Create holidays table
CREATE TABLE IF NOT EXISTS holidays (
id INT AUTO_INCREMENT PRIMARY KEY,
holiday_date DATE NOT NULL,
name_en VARCHAR(255) NOT NULL,
name_ar VARCHAR(255) NOT NULL,
is_recurring BOOLEAN DEFAULT FALSE
);
-- Create doctor schedules table (working hours)
CREATE TABLE IF NOT EXISTS doctor_schedules (
id INT AUTO_INCREMENT PRIMARY KEY,
doctor_id INT NOT NULL,
day_of_week INT NOT NULL, -- 0 (Sunday) to 6 (Saturday)
start_time TIME NOT NULL,
end_time TIME NOT NULL,
FOREIGN KEY (doctor_id) REFERENCES doctors(id) ON DELETE CASCADE
);
-- Seed some holidays
INSERT INTO holidays (holiday_date, name_en, name_ar, is_recurring) VALUES
('2026-01-01', 'New Year', 'رأس السنة', TRUE),
('2026-12-25', 'Christmas', 'عيد الميلاد', TRUE);

View File

@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS settings (
id INT AUTO_INCREMENT PRIMARY KEY,
setting_key VARCHAR(100) NOT NULL UNIQUE,
setting_value TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
INSERT INTO settings (setting_key, setting_value) VALUES
('company_name', 'Hospital Management System'),
('company_logo', ''),
('company_favicon', ''),
('company_ctr_no', ''),
('company_registration_no', ''),
('company_address', ''),
('company_phone', ''),
('company_email', ''),
('company_vat_no', '');

View File

@ -0,0 +1,38 @@
-- X-Ray Module Tables
CREATE TABLE IF NOT EXISTS xray_groups (
id INT AUTO_INCREMENT PRIMARY KEY,
name_en VARCHAR(255) NOT NULL,
name_ar VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS xray_tests (
id INT AUTO_INCREMENT PRIMARY KEY,
group_id INT,
name_en VARCHAR(255) NOT NULL,
name_ar VARCHAR(255) NOT NULL,
price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (group_id) REFERENCES xray_groups(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS xray_inquiries (
id INT AUTO_INCREMENT PRIMARY KEY,
patient_name VARCHAR(255) NOT NULL,
inquiry_date DATETIME DEFAULT CURRENT_TIMESTAMP,
source ENUM('Internal', 'External') DEFAULT 'Internal',
status ENUM('Pending', 'Completed', 'Cancelled') DEFAULT 'Pending',
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS xray_inquiry_items (
id INT AUTO_INCREMENT PRIMARY KEY,
inquiry_id INT NOT NULL,
xray_id INT NOT NULL,
result TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (inquiry_id) REFERENCES xray_inquiries(id) ON DELETE CASCADE,
FOREIGN KEY (xray_id) REFERENCES xray_tests(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@ -0,0 +1,15 @@
-- Update laboratory_inquiries and xray_inquiries to link with patients and visits
ALTER TABLE laboratory_inquiries ADD COLUMN patient_id INT NULL AFTER id;
ALTER TABLE laboratory_inquiries ADD COLUMN visit_id INT NULL AFTER patient_id;
ALTER TABLE laboratory_inquiries MODIFY patient_name VARCHAR(255) NULL;
ALTER TABLE laboratory_inquiries ADD CONSTRAINT fk_lab_patient FOREIGN KEY (patient_id) REFERENCES patients(id) ON DELETE CASCADE;
ALTER TABLE laboratory_inquiries ADD CONSTRAINT fk_lab_visit FOREIGN KEY (visit_id) REFERENCES visits(id) ON DELETE SET NULL;
ALTER TABLE xray_inquiries ADD COLUMN patient_id INT NULL AFTER id;
ALTER TABLE xray_inquiries ADD COLUMN visit_id INT NULL AFTER patient_id;
ALTER TABLE xray_inquiries MODIFY patient_name VARCHAR(255) NULL;
ALTER TABLE xray_inquiries ADD CONSTRAINT fk_xray_patient FOREIGN KEY (patient_id) REFERENCES patients(id) ON DELETE CASCADE;
ALTER TABLE xray_inquiries ADD CONSTRAINT fk_xray_visit FOREIGN KEY (visit_id) REFERENCES visits(id) ON DELETE SET NULL;

View File

@ -0,0 +1,49 @@
-- Seed X-Ray Groups
INSERT INTO xray_groups (name_en, name_ar) VALUES ('Chest', 'الصدر');
SET @chest_id = LAST_INSERT_ID();
INSERT INTO xray_groups (name_en, name_ar) VALUES ('Extremities', 'الأطراف');
SET @ext_id = LAST_INSERT_ID();
INSERT INTO xray_groups (name_en, name_ar) VALUES ('Spine', 'العمود الفقري');
SET @spine_id = LAST_INSERT_ID();
INSERT INTO xray_groups (name_en, name_ar) VALUES ('Abdomen', 'البطن');
SET @abd_id = LAST_INSERT_ID();
INSERT INTO xray_groups (name_en, name_ar) VALUES ('Dental', 'الأسنان');
SET @den_id = LAST_INSERT_ID();
INSERT INTO xray_groups (name_en, name_ar) VALUES ('Skull', 'الجمجمة');
SET @skl_id = LAST_INSERT_ID();
-- Seed X-Ray Tests
-- Chest
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@chest_id, 'Chest PA/Lateral', 'أشعة على الصدر - وضع أمامي جانبي', 150.00);
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@chest_id, 'Chest PA Only', 'أشعة على الصدر - وضع أمامي خلفي فقط', 100.00);
-- Extremities
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@ext_id, 'Hand AP/Lateral', 'أشعة على اليد', 120.00);
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@ext_id, 'Foot AP/Lateral', 'أشعة على القدم', 120.00);
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@ext_id, 'Knee AP/Lateral', 'أشعة على الركبة', 140.00);
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@ext_id, 'Shoulder AP/Lateral', 'أشعة على الكتف', 150.00);
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@ext_id, 'Elbow AP/Lateral', 'أشعة على الكوع', 120.00);
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@ext_id, 'Ankle AP/Lateral', 'أشعة على الكاحل', 120.00);
-- Spine
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@spine_id, 'Cervical Spine AP/Lateral', 'أشعة على الفقرات العنقية', 180.00);
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@spine_id, 'Lumbar Spine AP/Lateral', 'أشعة على الفقرات القطنية', 200.00);
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@spine_id, 'Thoracic Spine AP/Lateral', 'أشعة على الفقرات الصدرية', 180.00);
-- Abdomen
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@abd_id, 'Abdomen KUB', 'أشعة على البطن والمسالك البولية', 160.00);
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@abd_id, 'Abdomen Erect/Supine', 'أشعة على البطن', 160.00);
-- Dental
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@den_id, 'Dental Panoramic', 'أشعة بانوراما للأسنان', 250.00);
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@den_id, 'Periapical X-Ray', 'أشعة صغيرة للأسنان', 50.00);
-- Skull
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@skl_id, 'Skull AP/Lateral', 'أشعة على الجمجمة', 180.00);
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@skl_id, 'Paranasal Sinuses', 'أشعة على الجيوب الأنفية', 150.00);

View File

@ -0,0 +1,2 @@
-- Add attachment column to xray_inquiry_items to store uploaded images/results
ALTER TABLE xray_inquiry_items ADD COLUMN attachment VARCHAR(255) DEFAULT NULL;

View File

@ -0,0 +1,5 @@
-- Add civil_id, nationality, city to patients
ALTER TABLE patients
ADD COLUMN civil_id VARCHAR(50) DEFAULT NULL,
ADD COLUMN nationality VARCHAR(100) DEFAULT NULL,
ADD COLUMN city VARCHAR(100) DEFAULT NULL;

View File

@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS drugs_groups (
id INT AUTO_INCREMENT PRIMARY KEY,
name_en VARCHAR(255) NOT NULL,
name_ar VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS drugs (
id INT AUTO_INCREMENT PRIMARY KEY,
group_id INT,
name_en VARCHAR(255) NOT NULL,
name_ar VARCHAR(255) NOT NULL,
description_en TEXT,
description_ar TEXT,
default_dosage VARCHAR(255),
default_instructions TEXT,
price DECIMAL(10, 2) DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (group_id) REFERENCES drugs_groups(id) ON DELETE SET NULL
);

View File

@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS suppliers (
id INT AUTO_INCREMENT PRIMARY KEY,
name_en VARCHAR(255) NOT NULL,
name_ar VARCHAR(255) NOT NULL,
contact_person VARCHAR(255),
phone VARCHAR(50),
email VARCHAR(100),
address TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE drugs ADD COLUMN expiry_date DATE DEFAULT NULL;
ALTER TABLE drugs ADD COLUMN supplier_id INT DEFAULT NULL;
ALTER TABLE drugs ADD CONSTRAINT fk_drugs_supplier FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL;

View File

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS visit_prescriptions (
id INT AUTO_INCREMENT PRIMARY KEY,
visit_id INT NOT NULL,
drug_name VARCHAR(255) NOT NULL,
dosage VARCHAR(100),
instructions TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (visit_id) REFERENCES visits(id) ON DELETE CASCADE
);

View File

@ -0,0 +1,4 @@
ALTER TABLE drugs MODIFY name_en TEXT;
ALTER TABLE drugs MODIFY name_ar TEXT;
ALTER TABLE drugs_groups MODIFY name_en TEXT;
ALTER TABLE drugs_groups MODIFY name_ar TEXT;

View File

@ -0,0 +1,3 @@
ALTER TABLE employees ADD COLUMN IF NOT EXISTS position_id INT NULL;
ALTER TABLE employees DROP COLUMN IF EXISTS passion_en;
ALTER TABLE employees DROP COLUMN IF EXISTS passion_ar;

View File

@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS cities (
id INT AUTO_INCREMENT PRIMARY KEY,
name_en VARCHAR(100) NOT NULL,
name_ar VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO cities (name_en, name_ar) VALUES
('Muscat', 'مسقط'),
('Salalah', 'صلالة'),
('Sohar', 'صحار'),
('Nizwa', 'نزوى'),
('Sur', 'صور'),
('Al Buraimi', 'البريمي'),
('Seeb', 'السيب'),
('Bawshar', 'بوشر'),
('Ibri', 'عبري'),
('Rustaq', 'الرستاق'),
('Khasab', 'خصب'),
('Bahla', 'بهلاء');

View File

@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS services (
id INT AUTO_INCREMENT PRIMARY KEY,
name_en VARCHAR(255) NOT NULL,
name_ar VARCHAR(255) NOT NULL,
department_id INT NOT NULL,
price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@ -0,0 +1,24 @@
SET @dbname = DATABASE();
SET @tablename = "poisons";
SET @targetname = "positions";
SET @exists = (
SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES
WHERE table_schema = @dbname
AND table_name = @tablename
);
SET @target_exists = (
SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES
WHERE table_schema = @dbname
AND table_name = @targetname
);
SET @stmt = IF(@exists > 0 AND @target_exists = 0,
CONCAT('RENAME TABLE ', @tablename, ' TO ', @targetname),
'SELECT 1'
);
PREPARE stmt FROM @stmt;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@ -0,0 +1 @@
ALTER TABLE xray_inquiry_items MODIFY COLUMN attachment LONGTEXT;

View File

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS doctor_holidays (
id INT AUTO_INCREMENT PRIMARY KEY,
doctor_id INT NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
note VARCHAR(255) DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (doctor_id) REFERENCES doctors(id) ON DELETE CASCADE
);

View 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);

View File

@ -0,0 +1 @@
ALTER TABLE departments ADD COLUMN show_in_queue BOOLEAN DEFAULT 1;

View File

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS queue_ads (
id INT AUTO_INCREMENT PRIMARY KEY,
text_en TEXT NOT NULL,
text_ar TEXT NOT NULL,
active TINYINT(1) DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

View File

@ -0,0 +1,11 @@
-- Add columns to appointments table
ALTER TABLE appointments ADD COLUMN IF NOT EXISTS nurse_id INT NULL;
ALTER TABLE appointments ADD COLUMN IF NOT EXISTS visit_type ENUM('Clinic', 'Home') DEFAULT 'Clinic';
ALTER TABLE appointments ADD COLUMN IF NOT EXISTS address TEXT NULL;
ALTER TABLE appointments ADD CONSTRAINT fk_appointment_nurse FOREIGN KEY (nurse_id) REFERENCES nurses(id) ON DELETE SET NULL;
-- Add columns to visits table
ALTER TABLE visits ADD COLUMN IF NOT EXISTS nurse_id INT NULL;
ALTER TABLE visits ADD COLUMN IF NOT EXISTS visit_type ENUM('Clinic', 'Home') DEFAULT 'Clinic';
ALTER TABLE visits ADD COLUMN IF NOT EXISTS address TEXT NULL;
ALTER TABLE visits ADD CONSTRAINT fk_visit_nurse FOREIGN KEY (nurse_id) REFERENCES nurses(id) ON DELETE SET NULL;

View File

@ -0,0 +1,3 @@
ALTER TABLE bills ADD COLUMN IF NOT EXISTS insurance_covered DECIMAL(10,2) DEFAULT 0.00;
ALTER TABLE bills ADD COLUMN IF NOT EXISTS patient_payable DECIMAL(10,2) DEFAULT 0.00;
ALTER TABLE bills ADD COLUMN IF NOT EXISTS total_amount DECIMAL(10,2) DEFAULT 0.00;

View File

@ -0,0 +1,2 @@
ALTER TABLE bills ADD COLUMN IF NOT EXISTS payment_method ENUM('Cash', 'Card', 'Insurance', 'Online', 'Other') DEFAULT 'Cash';
ALTER TABLE bills ADD COLUMN IF NOT EXISTS notes TEXT;

View File

@ -0,0 +1,2 @@
ALTER TABLE drugs ADD COLUMN sku VARCHAR(50) DEFAULT NULL AFTER id;
CREATE INDEX idx_drugs_sku ON drugs(sku);

View File

@ -0,0 +1,2 @@
ALTER TABLE visits ADD COLUMN IF NOT EXISTS status ENUM('CheckIn', 'In Progress', 'Completed', 'Cancelled') DEFAULT 'CheckIn';
ALTER TABLE visits ADD COLUMN IF NOT EXISTS checkout_time DATETIME NULL;

View File

@ -0,0 +1,38 @@
-- Create roles table
CREATE TABLE IF NOT EXISTS roles (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE,
slug VARCHAR(50) NOT NULL UNIQUE,
permissions TEXT NULL, -- JSON or serialized array of permissions
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Create users table
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
role_id INT NOT NULL,
active TINYINT(1) DEFAULT 1,
last_login DATETIME NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Seed Roles
INSERT IGNORE INTO roles (name, slug, permissions) VALUES
('Administrator', 'admin', '*'),
('Doctor', 'doctor', '["dashboard", "patients", "visits", "appointments", "home_visits", "reports"]'),
('Nurse', 'nurse', '["dashboard", "patients", "visits", "queue"]'),
('Receptionist', 'receptionist', '["dashboard", "patients", "appointments", "queue", "billing"]'),
('Laboratorial', 'laboratorial', '["dashboard", "laboratory"]'),
('Radiologic', 'radiologic', '["dashboard", "xray"]');
-- Seed Default Admin User (password: admin123)
-- Using a simple hash for demonstration if PHP's password_hash is not available in SQL,
-- but ideally we should insert via PHP. For now, I will insert a placeholder and update it via PHP or assume I can use a known hash.
-- Hash for 'admin123' (bcrypt)
INSERT IGNORE INTO users (name, email, password, role_id)
SELECT 'System Admin', 'admin@hospital.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', id
FROM roles WHERE slug = 'admin' LIMIT 1;

View File

@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS insurance_payments (
id INT AUTO_INCREMENT PRIMARY KEY,
insurance_company_id INT NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
payment_date DATE NOT NULL,
reference_number VARCHAR(100) DEFAULT NULL,
notes TEXT DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (insurance_company_id) REFERENCES insurance_companies(id) ON DELETE CASCADE
);

View File

@ -0,0 +1,52 @@
CREATE TABLE IF NOT EXISTS inventory_categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name_en VARCHAR(255) NOT NULL,
name_ar VARCHAR(255) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS inventory_items (
id INT AUTO_INCREMENT PRIMARY KEY,
category_id INT,
name_en VARCHAR(255) NOT NULL,
name_ar VARCHAR(255) NOT NULL,
description TEXT,
sku VARCHAR(100) UNIQUE,
unit VARCHAR(50) DEFAULT 'piece',
min_level INT DEFAULT 10,
reorder_level INT DEFAULT 20,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES inventory_categories(id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS inventory_batches (
id INT AUTO_INCREMENT PRIMARY KEY,
item_id INT NOT NULL,
batch_number VARCHAR(100),
expiry_date DATE,
quantity INT NOT NULL DEFAULT 0,
cost_price DECIMAL(10, 2) DEFAULT 0.00,
supplier_id INT,
received_date DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (item_id) REFERENCES inventory_items(id) ON DELETE CASCADE,
FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS inventory_transactions (
id INT AUTO_INCREMENT PRIMARY KEY,
item_id INT NOT NULL,
batch_id INT,
transaction_type ENUM('in', 'out', 'adjustment') NOT NULL,
quantity INT NOT NULL,
reference_type VARCHAR(50), -- 'purchase', 'consumption', 'manual_adjustment'
reference_id INT,
user_id INT,
transaction_date DATETIME DEFAULT CURRENT_TIMESTAMP,
notes TEXT,
FOREIGN KEY (item_id) REFERENCES inventory_items(id) ON DELETE CASCADE,
FOREIGN KEY (batch_id) REFERENCES inventory_batches(id) ON DELETE SET NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
);

View File

@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS `pharmacy_lpos` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`supplier_id` INT NOT NULL,
`lpo_date` DATE NOT NULL,
`status` ENUM('Draft', 'Sent', 'Received', 'Cancelled') DEFAULT 'Draft',
`total_amount` DECIMAL(10, 2) DEFAULT 0.00,
`notes` TEXT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`supplier_id`) REFERENCES `suppliers`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `pharmacy_lpo_items` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`lpo_id` INT NOT NULL,
`drug_id` INT NOT NULL,
`quantity` INT NOT NULL,
`cost_price` DECIMAL(10, 2) NOT NULL,
`total_cost` DECIMAL(10, 2) NOT NULL,
FOREIGN KEY (`lpo_id`) REFERENCES `pharmacy_lpos`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`drug_id`) REFERENCES `drugs`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -0,0 +1,49 @@
-- Create pharmacy_batches table
CREATE TABLE IF NOT EXISTS pharmacy_batches (
id INT AUTO_INCREMENT PRIMARY KEY,
drug_id INT NOT NULL,
batch_number VARCHAR(50) NOT NULL,
expiry_date DATE NOT NULL,
quantity INT NOT NULL DEFAULT 0,
cost_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
sale_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
supplier_id INT NULL,
received_date DATE NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (drug_id) REFERENCES drugs(id) ON DELETE CASCADE,
FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL
);
-- Create pharmacy_sales table
CREATE TABLE IF NOT EXISTS pharmacy_sales (
id INT AUTO_INCREMENT PRIMARY KEY,
patient_id INT NULL,
visit_id INT NULL,
total_amount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
payment_method VARCHAR(50) DEFAULT 'cash',
status VARCHAR(20) DEFAULT 'completed', -- completed, refunded, cancelled
notes TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (patient_id) REFERENCES patients(id) ON DELETE SET NULL,
FOREIGN KEY (visit_id) REFERENCES visits(id) ON DELETE SET NULL
);
-- Create pharmacy_sale_items table
CREATE TABLE IF NOT EXISTS pharmacy_sale_items (
id INT AUTO_INCREMENT PRIMARY KEY,
sale_id INT NOT NULL,
drug_id INT NOT NULL,
batch_id INT NULL, -- Can be null if we track sales without specific batch selection (though we should enforce it for stock deduction)
quantity INT NOT NULL DEFAULT 1,
unit_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
total_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (sale_id) REFERENCES pharmacy_sales(id) ON DELETE CASCADE,
FOREIGN KEY (drug_id) REFERENCES drugs(id),
FOREIGN KEY (batch_id) REFERENCES pharmacy_batches(id) ON DELETE SET NULL
);
-- Add stock management columns to drugs table
ALTER TABLE drugs ADD COLUMN IF NOT EXISTS min_stock_level INT DEFAULT 10;
ALTER TABLE drugs ADD COLUMN IF NOT EXISTS reorder_level INT DEFAULT 20;
ALTER TABLE drugs ADD COLUMN IF NOT EXISTS unit VARCHAR(50) DEFAULT 'pack';

View File

@ -0,0 +1,29 @@
-- Add batch and expiry to LPO items for receiving stock
ALTER TABLE pharmacy_lpo_items ADD COLUMN IF NOT EXISTS batch_number VARCHAR(50) NULL;
ALTER TABLE pharmacy_lpo_items ADD COLUMN IF NOT EXISTS expiry_date DATE NULL;
-- Create Purchase Returns table
CREATE TABLE IF NOT EXISTS pharmacy_purchase_returns (
id INT AUTO_INCREMENT PRIMARY KEY,
supplier_id INT NOT NULL,
return_date DATE NOT NULL,
total_amount DECIMAL(10, 2) DEFAULT 0.00,
reason TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (supplier_id) REFERENCES suppliers(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Create Purchase Return Items table
CREATE TABLE IF NOT EXISTS pharmacy_purchase_return_items (
id INT AUTO_INCREMENT PRIMARY KEY,
return_id INT NOT NULL,
drug_id INT NOT NULL,
batch_id INT NULL, -- Which batch we are returning from
quantity INT NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL, -- Refund price
total_price DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (return_id) REFERENCES pharmacy_purchase_returns(id) ON DELETE CASCADE,
FOREIGN KEY (drug_id) REFERENCES drugs(id),
FOREIGN KEY (batch_id) REFERENCES pharmacy_batches(id) ON DELETE SET NULL
);

View File

@ -0,0 +1,2 @@
ALTER TABLE inventory_transactions ADD COLUMN IF NOT EXISTS department_id INT NULL;
ALTER TABLE inventory_transactions ADD CONSTRAINT fk_inventory_dept FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE SET NULL;

View File

@ -0,0 +1 @@
ALTER TABLE insurance_payments ADD COLUMN payment_method VARCHAR(50) DEFAULT 'Check' AFTER reference_number;

View File

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN avatar VARCHAR(255) DEFAULT NULL;

View File

@ -0,0 +1,2 @@
ALTER TABLE nurses ADD COLUMN IF NOT EXISTS employee_id INT;
ALTER TABLE nurses ADD CONSTRAINT fk_nurse_employee FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE SET NULL;

View File

@ -0,0 +1 @@
ALTER TABLE drugs ADD COLUMN image VARCHAR(255) NULL;

View File

@ -0,0 +1 @@
ALTER TABLE visits ADD COLUMN IF NOT EXISTS nursing_notes TEXT AFTER temperature;

View File

@ -0,0 +1,55 @@
-- Add user_id to employees to link with login
ALTER TABLE employees ADD COLUMN IF NOT EXISTS user_id INT NULL;
ALTER TABLE employees ADD COLUMN IF NOT EXISTS join_date DATE NULL;
-- Attendance
CREATE TABLE IF NOT EXISTS attendance_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
employee_id INT NOT NULL,
date DATE NOT NULL,
check_in DATETIME NULL,
check_out DATETIME NULL,
status ENUM('Present', 'Late', 'Absent', 'On Leave') DEFAULT 'Present',
source VARCHAR(50) DEFAULT 'Web',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE
);
-- Leaves
CREATE TABLE IF NOT EXISTS leave_requests (
id INT AUTO_INCREMENT PRIMARY KEY,
employee_id INT NOT NULL,
leave_type VARCHAR(50) NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
days INT NOT NULL,
reason TEXT,
status ENUM('Pending', 'Approved', 'Rejected') DEFAULT 'Pending',
approved_by INT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE
);
-- Salaries / Payroll Info
CREATE TABLE IF NOT EXISTS employee_salaries (
id INT AUTO_INCREMENT PRIMARY KEY,
employee_id INT NOT NULL,
basic_salary DECIMAL(10, 2) DEFAULT 0.00,
housing_allowance DECIMAL(10, 2) DEFAULT 0.00,
transport_allowance DECIMAL(10, 2) DEFAULT 0.00,
other_allowance DECIMAL(10, 2) DEFAULT 0.00,
currency VARCHAR(10) DEFAULT 'USD',
effective_date DATE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE
);
-- Biometric Devices (for API auth)
CREATE TABLE IF NOT EXISTS biometric_devices (
id INT AUTO_INCREMENT PRIMARY KEY,
device_name VARCHAR(100) NOT NULL,
ip_address VARCHAR(50),
api_key VARCHAR(255) NOT NULL,
status TINYINT(1) DEFAULT 1,
last_seen DATETIME NULL
);

View File

@ -0,0 +1,28 @@
-- Fix appointments table schema: Add start_time/end_time if missing
-- This handles the case where init_db.php created the table with appointment_date instead of start_time
-- Add start_time if it doesn't exist
-- Note: MySQL/MariaDB < 10.2 don't support ADD COLUMN IF NOT EXISTS easily, so we use a stored procedure or just suppress errors in apply_migrations.php
-- Assuming MariaDB 10.2+ or suppression in apply_migrations.php for "Duplicate column"
ALTER TABLE appointments ADD COLUMN start_time DATETIME NULL;
ALTER TABLE appointments ADD COLUMN end_time DATETIME NULL;
-- Migrate data if appointment_date exists
-- Check if appointment_date exists first? SQL doesn't have conditional logic outside procedures easily.
-- We'll try to update, if column doesn't exist it will fail but that's fine if start_time is already populated.
-- Wait, if appointment_date doesn't exist, this query will fail and might stop migration script if not handled.
-- But apply_migrations.php continues on error? No, it catches exceptions per statement.
-- We can wrap in a procedure to be safe, but apply_migrations.php splits by ';'.
-- So let's just try the update. If appointment_date doesn't exist, it fails harmlessly.
UPDATE appointments SET start_time = appointment_date WHERE start_time IS NULL;
UPDATE appointments SET end_time = DATE_ADD(start_time, INTERVAL 30 MINUTE) WHERE end_time IS NULL AND start_time IS NOT NULL;
-- Make start_time NOT NULL after populating
ALTER TABLE appointments MODIFY COLUMN start_time DATETIME NOT NULL;
ALTER TABLE appointments MODIFY COLUMN end_time DATETIME NOT NULL;
-- Drop appointment_date if it exists (optional cleanup)
-- ALTER TABLE appointments DROP COLUMN appointment_date;

View File

@ -0,0 +1,64 @@
-- Migration to merge Doctors and Nurses into HR (Employees)
-- Step 1: Add new columns to hold Employee IDs
ALTER TABLE visits ADD COLUMN IF NOT EXISTS doctor_employee_id INT NULL;
ALTER TABLE appointments ADD COLUMN IF NOT EXISTS doctor_employee_id INT NULL;
ALTER TABLE appointments ADD COLUMN IF NOT EXISTS nurse_employee_id INT NULL;
-- Step 2: Migrate data (if doctors/nurses have employee_id set)
-- Update Visits
UPDATE visits v
JOIN doctors d ON v.doctor_id = d.id
SET v.doctor_employee_id = d.employee_id
WHERE d.employee_id IS NOT NULL;
-- Update Appointments (Doctor)
UPDATE appointments a
JOIN doctors d ON a.doctor_id = d.id
SET a.doctor_employee_id = d.employee_id
WHERE d.employee_id IS NOT NULL;
-- Update Appointments (Nurse)
UPDATE appointments a
JOIN nurses n ON a.nurse_id = n.id
SET a.nurse_employee_id = n.employee_id
WHERE n.employee_id IS NOT NULL;
-- Step 3: Drop old Foreign Keys (Constraint names might vary, so we try standard names or rely on DROP COLUMN to drop FKs in some DBs, but explicitly dropping FK is safer)
-- Finding constraint names is hard in SQL script without dynamic SQL.
-- However, in MariaDB/MySQL, dropping the column usually drops the FK.
-- But to be safe, we will try to drop the standard named constraints if known, or just proceed with DROP COLUMN which should work if no other constraints block it.
ALTER TABLE visits DROP FOREIGN KEY IF EXISTS visits_ibfk_2; -- doctor_id
ALTER TABLE visits DROP FOREIGN KEY IF EXISTS fk_visit_nurse;
ALTER TABLE visits DROP FOREIGN KEY IF EXISTS fk_visit_doctor_employee;
ALTER TABLE appointments DROP FOREIGN KEY IF EXISTS appointments_ibfk_2; -- doctor_id
ALTER TABLE appointments DROP FOREIGN KEY IF EXISTS appointments_ibfk_3; -- nurse_id
ALTER TABLE appointments DROP FOREIGN KEY IF EXISTS fk_appointment_nurse;
ALTER TABLE appointments DROP FOREIGN KEY IF EXISTS fk_appt_doctor_employee;
ALTER TABLE appointments DROP FOREIGN KEY IF EXISTS fk_appt_nurse_employee;
-- Also drop keys/indexes if they exist separate from FK
ALTER TABLE visits DROP KEY IF EXISTS doctor_id;
ALTER TABLE appointments DROP KEY IF EXISTS doctor_id;
ALTER TABLE appointments DROP KEY IF EXISTS nurse_id;
-- Step 4: Drop old columns
ALTER TABLE visits DROP COLUMN doctor_id;
ALTER TABLE appointments DROP COLUMN doctor_id;
ALTER TABLE appointments DROP COLUMN nurse_id;
-- Step 5: Rename new columns to match standard naming (or keep them and add FK)
-- Let's rename them back to doctor_id and nurse_id but now they point to employees
ALTER TABLE visits CHANGE COLUMN doctor_employee_id doctor_id INT NULL;
ALTER TABLE appointments CHANGE COLUMN doctor_employee_id doctor_id INT NULL;
ALTER TABLE appointments CHANGE COLUMN nurse_employee_id nurse_id INT NULL;
-- Step 6: Add new Foreign Keys to employees
ALTER TABLE visits ADD CONSTRAINT fk_visit_doctor_employee FOREIGN KEY (doctor_id) REFERENCES employees(id) ON DELETE SET NULL;
ALTER TABLE appointments ADD CONSTRAINT fk_appt_doctor_employee FOREIGN KEY (doctor_id) REFERENCES employees(id) ON DELETE SET NULL;
ALTER TABLE appointments ADD CONSTRAINT fk_appt_nurse_employee FOREIGN KEY (nurse_id) REFERENCES employees(id) ON DELETE SET NULL;
-- Step 7: Drop obsolete tables
DROP TABLE IF EXISTS doctor_holidays; -- If exists
DROP TABLE IF EXISTS doctors;
DROP TABLE IF EXISTS nurses;

View File

@ -0,0 +1,25 @@
-- Final cleanup for Doctors/Nurses migration
-- Fix Patient Queue Doctor ID (References doctors)
ALTER TABLE patient_queue ADD COLUMN IF NOT EXISTS doctor_employee_id INT NULL;
-- Migrate data
UPDATE patient_queue q
JOIN doctors d ON q.doctor_id = d.id
SET q.doctor_employee_id = d.employee_id
WHERE d.employee_id IS NOT NULL;
-- Drop old FK
ALTER TABLE patient_queue DROP FOREIGN KEY IF EXISTS patient_queue_ibfk_3;
-- Drop old column
ALTER TABLE patient_queue DROP COLUMN doctor_id;
-- Rename new column
ALTER TABLE patient_queue CHANGE COLUMN doctor_employee_id doctor_id INT NULL;
-- Add new FK to employees
ALTER TABLE patient_queue ADD CONSTRAINT fk_queue_doctor_employee FOREIGN KEY (doctor_id) REFERENCES employees(id) ON DELETE SET NULL;
-- Now drop doctors table
DROP TABLE IF EXISTS doctors;

View File

@ -0,0 +1,58 @@
-- Fix Migration: Merge Doctors and Nurses into HR (Employees) - Cleanup
-- 1. Drop dependent tables that reference doctors
DROP TABLE IF EXISTS doctor_schedules;
-- 2. Fix Visits Nurse ID
-- Add temp column if it doesn't exist (it wasn't added in previous migration)
ALTER TABLE visits ADD COLUMN IF NOT EXISTS nurse_employee_id INT NULL;
-- Migrate data from nurses table to visits.nurse_employee_id
UPDATE visits v
JOIN nurses n ON v.nurse_id = n.id
SET v.nurse_employee_id = n.employee_id
WHERE n.employee_id IS NOT NULL;
-- Drop old FK on visits.nurse_id
ALTER TABLE visits DROP FOREIGN KEY IF EXISTS fk_visit_nurse;
-- Drop old column visits.nurse_id
-- We use a check to avoid error if it was already dropped (though unlikely)
ALTER TABLE visits DROP COLUMN nurse_id;
-- Rename new column to nurse_id
ALTER TABLE visits CHANGE COLUMN nurse_employee_id nurse_id INT NULL;
-- Add new FK to employees
ALTER TABLE visits DROP FOREIGN KEY IF EXISTS fk_visit_nurse_employee;
ALTER TABLE visits ADD CONSTRAINT fk_visit_nurse_employee FOREIGN KEY (nurse_id) REFERENCES employees(id) ON DELETE SET NULL;
-- 3. Fix Appointments Nurse ID
-- Ensure nurse_employee_id exists (might have been created in previous migration)
ALTER TABLE appointments ADD COLUMN IF NOT EXISTS nurse_employee_id INT NULL;
-- Migrate data again just in case
UPDATE appointments a
JOIN nurses n ON a.nurse_id = n.id
SET a.nurse_employee_id = n.employee_id
WHERE n.employee_id IS NOT NULL;
-- Drop old FK on appointments.nurse_id
ALTER TABLE appointments DROP FOREIGN KEY IF EXISTS fk_appointment_nurse;
-- Drop old column appointments.nurse_id
ALTER TABLE appointments DROP COLUMN nurse_id;
-- Rename new column to nurse_id
ALTER TABLE appointments CHANGE COLUMN nurse_employee_id nurse_id INT NULL;
-- Add new FK to employees
ALTER TABLE appointments DROP FOREIGN KEY IF EXISTS fk_appt_nurse_employee;
ALTER TABLE appointments ADD CONSTRAINT fk_appt_nurse_employee FOREIGN KEY (nurse_id) REFERENCES employees(id) ON DELETE SET NULL;
-- 4. Drop obsolete tables
-- Now that FKs are gone, this should succeed.
DROP TABLE IF EXISTS doctors;
DROP TABLE IF EXISTS nurses;

View File

@ -0,0 +1 @@
ALTER TABLE departments ADD COLUMN active BOOLEAN DEFAULT 1;

View File

@ -0,0 +1 @@
ALTER TABLE employees ADD COLUMN room_number VARCHAR(50) NULL AFTER email;

View File

@ -0,0 +1,3 @@
ALTER TABLE departments ADD COLUMN requires_vitals TINYINT(1) DEFAULT 0;
ALTER TABLE departments ADD COLUMN is_vitals_room TINYINT(1) DEFAULT 0;
ALTER TABLE patient_queue ADD COLUMN target_department_id INT(11) DEFAULT NULL;

View File

@ -0,0 +1,12 @@
-- Seed positions and employees if none exist (useful for fresh installs)
INSERT IGNORE INTO positions (id, name_en, name_ar, description_en, description_ar) VALUES
(1, 'Doctor', 'طبيب', 'Medical Doctor', 'طبيب بشري'),
(2, 'Nurse', 'ممرض', 'Registered Nurse', 'ممرض مسجل'),
(3, 'Receptionist', 'موظف استقبال', 'Front Desk', 'الاستقبال');
INSERT IGNORE INTO employees (id, name_en, name_ar, email, mobile, department_id, position_id, room_number) VALUES
(1, 'Dr. Ahmed Ali', 'د. أحمد علي', 'ahmed@hospital.com', '0501234567', 1, 1, '101'),
(2, 'Dr. Sarah Smith', 'د. سارة سميث', 'sarah@hospital.com', '0501234568', 2, 1, '102'),
(3, 'Dr. John Doe', 'د. جون دو', 'john@hospital.com', '0501234569', 4, 1, '103'),
(4, 'Nurse Fatima', 'الممرضة فاطمة', 'fatima@hospital.com', '0501234570', 3, 2, 'ER-1'),
(5, 'Nurse Mary', 'الممرضة ماري', 'mary@hospital.com', '0501234571', 1, 2, 'OPD-1');

37
debug_queue.php Normal file
View 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>";
}

View File

@ -0,0 +1,10 @@
<?php
require_once __DIR__ . '/db/config.php';
$db = db();
// Delete the test appointment created by test_create_appointment_curl.php (ID 14)
// Use a safe check to only delete if it looks like a test
$id = 14;
$stmt = $db->prepare("DELETE FROM appointments WHERE id = ? AND reason = 'Test API'");
$stmt->execute([$id]);
echo "Deleted test appointment $id (if it matched).\n";

View File

@ -0,0 +1,12 @@
<?php
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/helpers.php';
$db = db();
$id = 15;
$stmt = $db->prepare("DELETE FROM appointments WHERE id = ?");
$stmt->execute([$id]);
echo "Deleted appointment $id\n";

16
departments.php Normal file
View File

@ -0,0 +1,16 @@
<?php
$section = 'departments';
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/includes/auth.php';
check_auth();
$db = db();
$lang = $_SESSION['lang'];
require_once __DIR__ . '/includes/actions.php';
require_once __DIR__ . '/includes/common_data.php';
require_once __DIR__ . '/includes/layout/header.php';
require_once __DIR__ . '/includes/pages/departments.php';
require_once __DIR__ . '/includes/layout/footer.php';

22
drugs.php Normal file
View File

@ -0,0 +1,22 @@
<?php
$section = 'drugs';
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/includes/auth.php';
check_auth();
$db = db();
$lang = $_SESSION['lang'] ?? 'en';
require_once __DIR__ . '/includes/actions.php';
require_once __DIR__ . '/includes/common_data.php';
if (!isset($_GET['ajax_search'])) {
require_once __DIR__ . '/includes/layout/header.php';
}
require_once __DIR__ . '/includes/pages/drugs.php';
if (!isset($_GET['ajax_search'])) {
require_once __DIR__ . '/includes/layout/footer.php';
}

24
drugs_groups.php Normal file
View File

@ -0,0 +1,24 @@
<?php
$section = 'drugs_groups';
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/includes/auth.php';
check_auth();
$db = db();
$lang = $_SESSION['lang'] ?? 'en';
require_once __DIR__ . '/includes/actions.php';
require_once __DIR__ . '/includes/common_data.php';
$is_ajax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
if (!$is_ajax) {
require_once __DIR__ . '/includes/layout/header.php';
}
require_once __DIR__ . '/includes/pages/drugs_groups.php';
if (!$is_ajax) {
require_once __DIR__ . '/includes/layout/footer.php';
}

26
employees.php Normal file
View File

@ -0,0 +1,26 @@
<?php
session_start();
if (!isset($_SESSION['lang'])) {
$_SESSION['lang'] = 'en';
}
if (isset($_GET['lang'])) {
$_SESSION['lang'] = $_GET['lang'] === 'ar' ? 'ar' : 'en';
}
require_once 'db/config.php';
require_once 'lang.php';
require_once 'helpers.php';
require_once __DIR__ . '/includes/auth.php';
check_auth();
$db = db();
$lang = $_SESSION['lang'];
$section = 'employees';
require_once 'includes/actions.php';
require_once 'includes/common_data.php';
include 'includes/layout/header.php';
include 'includes/pages/employees.php';
include 'includes/layout/footer.php';

132
helpers.php Normal file
View File

@ -0,0 +1,132 @@
<?php
$SYSTEM_SETTINGS = null;
function get_system_settings() {
global $db, $SYSTEM_SETTINGS;
if ($SYSTEM_SETTINGS !== null) {
return $SYSTEM_SETTINGS;
}
if (!isset($db)) {
require_once __DIR__ . '/db/config.php';
try {
$local_db = db();
} catch (Exception $e) {
// If DB connection fails, return empty settings instead of crashing
return [];
}
} else {
$local_db = $db;
}
try {
$stmt = $local_db->query('SELECT setting_key, setting_value FROM settings');
$settings = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$settings[$row['setting_key']] = $row['setting_value'];
}
$SYSTEM_SETTINGS = $settings;
return $settings;
} catch (Exception $e) {
return [];
}
}
function apply_timezone() {
try {
$s = get_system_settings();
if (!empty($s['timezone'])) {
date_default_timezone_set($s['timezone']);
}
} catch (Exception $e) {
// Ignore timezone errors
}
}
apply_timezone();
function format_currency($amount) {
$settings = get_system_settings();
$currency_symbol = $settings['currency_symbol'] ?? '$';
$decimal_digits = isset($settings['decimal_digits']) ? (int)$settings['decimal_digits'] : 2;
return $currency_symbol . ' ' . number_format((float)$amount, $decimal_digits);
}
// Only start session if not already started
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/lang.php';
if (!isset($_SESSION['lang'])) {
$_SESSION['lang'] = 'en';
}
if (isset($_GET['lang'])) {
if ($_GET['lang'] === 'ar' || $_GET['lang'] === 'en') {
$_SESSION['lang'] = $_GET['lang'];
// Redirect to remove lang param
if (!headers_sent()) {
header("Location: " . strtok($_SERVER["REQUEST_URI"], '?'));
exit;
}
}
}
function __($key) {
global $translations;
$lang = $_SESSION['lang'] ?? 'en'; // Fallback if session is empty
return $translations[$lang][$key] ?? $key;
}
function is_rtl() {
return ($_SESSION['lang'] ?? 'en') === 'ar';
}
function get_dir() {
return is_rtl() ? 'rtl' : 'ltr';
}
function get_lang_name() {
return ($_SESSION['lang'] ?? 'en') === 'ar' ? 'English' : 'العربية';
}
function get_lang_code() {
return $_SESSION['lang'] ?? 'en';
}
function calculate_age($dob) {
if (empty($dob)) return '-';
try {
$birthDate = new DateTime($dob);
$today = new DateTime('today');
return $birthDate->diff($today)->y;
} catch (Exception $e) {
return '-';
}
}
if (!function_exists('mb_strimwidth')) {
function mb_strimwidth($string, $start, $width, $trimmarker = '...', $encoding = null) {
// Simple polyfill using substr
// 1. Handle start offset
$string = (string)$string;
if ($start > 0) {
$string = substr($string, $start);
}
// 2. Check length
if (strlen($string) <= $width) {
return $string;
}
// 3. Truncate
$targetLen = $width - strlen($trimmarker);
if ($targetLen < 0) $targetLen = 0;
return substr($string, 0, $targetLen) . $trimmarker;
}
}

10
home_visits.php Normal file
View File

@ -0,0 +1,10 @@
<?php
require_once __DIR__ . '/includes/layout/header.php';
// Check for user role if needed (e.g. only doctors/admins/nurses)
// For now, accessible to all logged in users
require_once __DIR__ . '/includes/pages/home_visits.php';
require_once __DIR__ . '/includes/layout/footer.php';
?>

60
hospital_services.php Normal file
View File

@ -0,0 +1,60 @@
<?php
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/includes/auth.php';
check_auth();
$db = db();
// Handle Form Submissions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['action'])) {
try {
if ($_POST['action'] === 'add_service') {
$stmt = $db->prepare("INSERT INTO services (name_en, name_ar, department_id, price, is_active) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([
$_POST['name_en'],
$_POST['name_ar'],
$_POST['department_id'],
$_POST['price'],
isset($_POST['is_active']) ? 1 : 0
]);
$_SESSION['flash_message'] = '<div class="alert alert-success">' . __('service_added_successfully') . '</div>';
} elseif ($_POST['action'] === 'edit_service') {
$stmt = $db->prepare("UPDATE services SET name_en = ?, name_ar = ?, department_id = ?, price = ?, is_active = ? WHERE id = ?");
$stmt->execute([
$_POST['name_en'],
$_POST['name_ar'],
$_POST['department_id'],
$_POST['price'],
isset($_POST['is_active']) ? 1 : 0,
$_POST['id']
]);
$_SESSION['flash_message'] = '<div class="alert alert-success">' . __('service_updated_successfully') . '</div>';
} elseif ($_POST['action'] === 'delete_service') {
$stmt = $db->prepare("DELETE FROM services WHERE id = ?");
$stmt->execute([$_POST['id']]);
$_SESSION['flash_message'] = '<div class="alert alert-success">' . __('service_deleted_successfully') . '</div>';
}
// Redirect after successful operation
header("Location: hospital_services.php");
exit;
} catch (PDOException $e) {
$_SESSION['flash_message'] = '<div class="alert alert-danger">' . __('error') . ': ' . $e->getMessage() . '</div>';
// Redirect even on error, so the user sees the message
header("Location: hospital_services.php");
exit;
}
}
}
// Session check logic (if needed in future)
// if (!isset($_SESSION['user_id'])) { ... }
$section = 'services';
$title = __('services');
require_once __DIR__ . '/includes/layout/header.php';
require_once __DIR__ . '/includes/pages/services.php';
require_once __DIR__ . '/includes/layout/footer.php';

32
hr_attendance.php Normal file
View File

@ -0,0 +1,32 @@
<?php
// Enable detailed error reporting for debugging
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$section = 'hr_attendance';
require_once __DIR__ . '/db/config.php';
// Try to connect to DB first
try {
$db = db();
} catch (PDOException $e) {
die("Database Connection Error: " . $e->getMessage());
} catch (Exception $e) {
die("General Error: " . $e->getMessage());
}
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/includes/auth.php';
check_auth();
$lang = $_SESSION['lang'];
require_once __DIR__ . '/includes/actions.php';
require_once __DIR__ . '/includes/common_data.php';
require_once __DIR__ . '/includes/layout/header.php';
require_once __DIR__ . '/includes/pages/hr_attendance.php';
require_once __DIR__ . '/includes/layout/footer.php';

32
hr_dashboard.php Normal file
View File

@ -0,0 +1,32 @@
<?php
// Enable detailed error reporting for debugging
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$section = 'hr_dashboard';
require_once __DIR__ . '/db/config.php';
// Try to connect to DB first
try {
$db = db();
} catch (PDOException $e) {
die("Database Connection Error: " . $e->getMessage());
} catch (Exception $e) {
die("General Error: " . $e->getMessage());
}
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/includes/auth.php';
check_auth();
$lang = $_SESSION['lang'];
require_once __DIR__ . '/includes/actions.php';
require_once __DIR__ . '/includes/common_data.php';
require_once __DIR__ . '/includes/layout/header.php';
require_once __DIR__ . '/includes/pages/hr_dashboard.php';
require_once __DIR__ . '/includes/layout/footer.php';

32
hr_leaves.php Normal file
View File

@ -0,0 +1,32 @@
<?php
// Enable detailed error reporting for debugging
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$section = 'hr_leaves';
require_once __DIR__ . '/db/config.php';
// Try to connect to DB first
try {
$db = db();
} catch (PDOException $e) {
die("Database Connection Error: " . $e->getMessage());
} catch (Exception $e) {
die("General Error: " . $e->getMessage());
}
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/includes/auth.php';
check_auth();
$lang = $_SESSION['lang'];
require_once __DIR__ . '/includes/actions.php';
require_once __DIR__ . '/includes/common_data.php';
require_once __DIR__ . '/includes/layout/header.php';
require_once __DIR__ . '/includes/pages/hr_leaves.php';
require_once __DIR__ . '/includes/layout/footer.php';

1170
includes/SimpleXLSX.php Normal file

File diff suppressed because it is too large Load Diff

1245
includes/actions.php Normal file

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More