Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
559fedf5a0 | ||
|
|
c26368f032 | ||
|
|
9a0a41e1cd | ||
|
|
913366b874 | ||
|
|
b19e996e1e | ||
|
|
83aa809d57 | ||
|
|
d22f5a1d5d | ||
|
|
69846c8387 | ||
|
|
8f9c7c6314 | ||
|
|
e09626b316 | ||
|
|
e150954bb6 | ||
|
|
5eaa8cc290 | ||
|
|
9667cec7e6 | ||
|
|
3ca5cde9e3 | ||
|
|
620485b60a | ||
|
|
fbacfd6e29 | ||
|
|
0690a23bb7 | ||
|
|
5f99108a0d | ||
|
|
132df22fd4 | ||
|
|
97ae4ba490 | ||
|
|
329da3e5d5 | ||
|
|
a03fa77672 | ||
|
|
04bd70e7d8 | ||
|
|
bcd593fb90 | ||
|
|
e73384ddbc | ||
|
|
0d5768a3f8 | ||
|
|
4472d09232 | ||
|
|
6933c13c3e | ||
|
|
f62878214d | ||
|
|
10338c6bc6 | ||
|
|
b02c277bb8 | ||
|
|
5f98198b67 | ||
|
|
f10cdedce9 | ||
|
|
70cf52dd48 | ||
|
|
07e733df85 | ||
|
|
23c927161f | ||
|
|
3912cd5bd2 | ||
|
|
6387f7c226 | ||
|
|
5e776c9e2b | ||
|
|
68751660aa | ||
|
|
d96399430d | ||
|
|
66038df9e4 | ||
|
|
1cb1f89067 | ||
|
|
a6e75b0397 | ||
|
|
2bc76dec94 | ||
|
|
dc081ef86a | ||
|
|
b5ea341aa5 | ||
|
|
dad73767a1 | ||
|
|
fd41c8937a | ||
|
|
bab61e3570 | ||
|
|
641316f659 | ||
|
|
01f56287c6 | ||
|
|
43f495d39b | ||
|
|
9a2dea5596 | ||
|
|
2575e8e91e | ||
|
|
e4e7b4a246 | ||
|
|
adc8aae5d0 | ||
|
|
4f4d8efa33 | ||
|
|
4ea57c7524 | ||
|
|
5420bde76a | ||
|
|
4f1232c067 | ||
|
|
5f78c2abad | ||
|
|
0a3eff3c92 | ||
|
|
d79aa1e948 | ||
|
|
523db02f6f | ||
|
|
230a62f266 | ||
|
|
2b7bb7957e | ||
|
|
6439167fd2 | ||
|
|
48924b82af | ||
|
|
4d100ba658 | ||
|
|
d04323d02a | ||
|
|
e15aadbaf9 | ||
|
|
defcfeb7da | ||
|
|
c3784f3565 | ||
|
|
c8679068a9 | ||
|
|
c6904a9a5e | ||
|
|
d3172b6c89 | ||
|
|
6f43ba8047 | ||
|
|
933409e6cf | ||
|
|
a63d31ec70 | ||
|
|
63a866d898 | ||
|
|
85f641cde7 | ||
|
|
aa6a7b2ed1 |
13
.gemini/GEMINI_LAST_MESSAGE.txt
Normal 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
@ -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).
|
||||
@ -1,41 +1,63 @@
|
||||
<?php
|
||||
// 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');
|
||||
$projectId = getenv('PROJECT_ID');
|
||||
// Helper to check env vars from multiple sources
|
||||
function get_env_var($key) {
|
||||
$val = getenv($key);
|
||||
if ($val !== false && $val !== '' && $val !== null) return $val;
|
||||
if (isset($_SERVER[$key]) && $_SERVER[$key] !== '') return $_SERVER[$key];
|
||||
if (isset($_ENV[$key]) && $_ENV[$key] !== '') return $_ENV[$key];
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
($projectUuid === false || $projectUuid === null || $projectUuid === '') ||
|
||||
($projectId === false || $projectId === null || $projectId === '')
|
||||
) {
|
||||
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
|
||||
if ($envPath && is_readable($envPath)) {
|
||||
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||
$projectUuid = get_env_var('PROJECT_UUID');
|
||||
$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;
|
||||
}
|
||||
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) === '') {
|
||||
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;
|
||||
}
|
||||
}
|
||||
$projectUuid = getenv('PROJECT_UUID');
|
||||
$projectId = getenv('PROJECT_ID');
|
||||
// Stop after first successful .env load
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$projectUuid = ($projectUuid === false) ? null : $projectUuid;
|
||||
$projectId = ($projectId === false) ? null : $projectId;
|
||||
// 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';
|
||||
$responsesPath = $projectId ? "/projects/{$projectId}/ai-request" : null;
|
||||
|
||||
80
api/ai_report.php
Normal 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
@ -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
@ -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
@ -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()]);
|
||||
}
|
||||
48
api/cities.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?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' || $action === 'add_city') {
|
||||
$name_en = $input['name_en'] ?? $input['name'] ?? '';
|
||||
$name_ar = $input['name_ar'] ?? $input['name'] ?? '';
|
||||
|
||||
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();
|
||||
|
||||
$lang = $_SESSION['lang'] ?? 'en';
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'id' => $id,
|
||||
'name_en' => $name_en,
|
||||
'name_ar' => $name_ar,
|
||||
'city' => [
|
||||
'id' => $id,
|
||||
'name' => ($lang === 'ar' && !empty($name_ar)) ? $name_ar : $name_en
|
||||
]
|
||||
]);
|
||||
} 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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';
|
||||
BIN
assets/images/favicon_1774793140.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
assets/images/logo_1772630133.png
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
assets/images/logo_1772630277.png
Normal file
|
After Width: | Height: | Size: 246 KiB |
BIN
assets/images/logo_1774793140.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
83
assets/js/ai_helper.js
Normal 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;
|
||||
@ -1,8 +1,10 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- Chat Widget Logic ---
|
||||
const chatForm = document.getElementById('chat-form');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
|
||||
if (chatForm && chatInput && chatMessages) {
|
||||
const appendMessage = (text, sender) => {
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.classList.add('message', sender);
|
||||
@ -36,4 +38,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
BIN
assets/uploads/users/user_1_1774149432.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 246 KiB |
BIN
assets/uploads/xrays/1773570971_ss.pdf
Normal file
BIN
assets/uploads/xrays/1773582607_69b6b90f692da_scale.jfif
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
assets/uploads/xrays/1773582628_69b6b9245162b_ورد1.jfif
Normal file
|
After Width: | Height: | Size: 12 KiB |
16
billing.php
Normal 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';
|
||||
16
cities.php
Normal 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';
|
||||
5
cookies.txt
Normal file
@ -0,0 +1,5 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
127.0.0.1 FALSE / FALSE 0 PHPSESSID hvh0shk5nt5h4nbcfvo601cpjc
|
||||
42
dashboard.php
Normal 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';
|
||||
}
|
||||
@ -1,17 +1,20 @@
|
||||
<?php
|
||||
// Generated by setup_mariadb_project.sh — edit as needed.
|
||||
define('DB_HOST', '127.0.0.1');
|
||||
define('DB_NAME', 'app_38960');
|
||||
define('DB_USER', 'app_38960');
|
||||
define('DB_PASS', '36fb441e-8408-4101-afdc-7911dc065e36');
|
||||
if (!defined('DB_HOST')) define('DB_HOST', getenv('DB_HOST') ?: '127.0.0.1');
|
||||
if (!defined('DB_NAME')) define('DB_NAME', getenv('DB_NAME') ?: 'app_38960');
|
||||
if (!defined('DB_USER')) define('DB_USER', getenv('DB_USER') ?: 'app_38960');
|
||||
if (!defined('DB_PASS')) define('DB_PASS', getenv('DB_PASS') ?: '36fb441e-8408-4101-afdc-7911dc065e36');
|
||||
|
||||
if (!function_exists('db')) {
|
||||
function db() {
|
||||
static $pdo;
|
||||
if (!$pdo) {
|
||||
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
|
||||
]);
|
||||
}
|
||||
return $pdo;
|
||||
}
|
||||
}
|
||||
|
||||
30
db/migrations/20260304_add_attachment_to_lab_tests.sql
Normal 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;
|
||||
37
db/migrations/20260304_create_appointments_module.sql
Normal 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);
|
||||
18
db/migrations/20260304_create_settings_table.sql
Normal 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', '');
|
||||
38
db/migrations/20260304_create_xray_module.sql
Normal 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;
|
||||
15
db/migrations/20260304_link_inquiries_to_visits.sql
Normal 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;
|
||||
49
db/migrations/20260304_seed_xray_data.sql
Normal 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);
|
||||
2
db/migrations/20260304_z_add_attachment_to_xrays.sql
Normal 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;
|
||||
5
db/migrations/20260305_add_details_to_patients.sql
Normal 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;
|
||||
20
db/migrations/20260305_create_drugs_tables.sql
Normal 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
|
||||
);
|
||||
14
db/migrations/20260305_create_suppliers_module.sql
Normal 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;
|
||||
@ -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
|
||||
);
|
||||
4
db/migrations/20260305_z_alter_drugs_name_length.sql
Normal 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;
|
||||
3
db/migrations/20260306_change_passion_to_position.sql
Normal 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;
|
||||
20
db/migrations/20260306_create_cities_table.sql
Normal 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', 'بهلاء');
|
||||
11
db/migrations/20260306_create_services_module.sql
Normal 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;
|
||||
24
db/migrations/20260306_rename_poisons_to_positions.sql
Normal 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;
|
||||
@ -0,0 +1 @@
|
||||
ALTER TABLE xray_inquiry_items MODIFY COLUMN attachment LONGTEXT;
|
||||
9
db/migrations/20260315_create_doctor_holidays.sql
Normal 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
|
||||
);
|
||||
19
db/migrations/20260316_create_patient_queue.sql
Normal file
@ -0,0 +1,19 @@
|
||||
-- Create patient_queue table
|
||||
CREATE TABLE IF NOT EXISTS patient_queue (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
patient_id INT NOT NULL,
|
||||
department_id INT NOT NULL,
|
||||
doctor_id INT NULL,
|
||||
visit_id INT NULL,
|
||||
token_number INT NOT NULL,
|
||||
status ENUM('waiting', 'serving', 'completed', 'cancelled') DEFAULT 'waiting',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (patient_id) REFERENCES patients(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (doctor_id) REFERENCES doctors(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (visit_id) REFERENCES visits(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- Index for faster searching of today's queue
|
||||
CREATE INDEX idx_queue_date_dept ON patient_queue(created_at, department_id);
|
||||
@ -0,0 +1 @@
|
||||
ALTER TABLE departments ADD COLUMN show_in_queue BOOLEAN DEFAULT 1;
|
||||
7
db/migrations/20260317_create_queue_ads.sql
Normal 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
|
||||
);
|
||||
11
db/migrations/20260317_enable_home_visits.sql
Normal 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;
|
||||
3
db/migrations/20260321_add_billing_columns.sql
Normal 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;
|
||||
2
db/migrations/20260321_add_payment_method_to_bills.sql
Normal 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;
|
||||
2
db/migrations/20260321_add_sku_to_drugs.sql
Normal 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);
|
||||
2
db/migrations/20260321_add_status_to_visits.sql
Normal 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;
|
||||
38
db/migrations/20260321_create_auth_system.sql
Normal 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;
|
||||
10
db/migrations/20260321_create_insurance_payments.sql
Normal 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
|
||||
);
|
||||
52
db/migrations/20260321_create_inventory_module.sql
Normal 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
|
||||
);
|
||||
21
db/migrations/20260321_create_pharmacy_lpo.sql
Normal 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;
|
||||
49
db/migrations/20260321_create_pharmacy_module.sql
Normal 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';
|
||||
29
db/migrations/20260321_create_purchase_returns.sql
Normal 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
|
||||
);
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
ALTER TABLE insurance_payments ADD COLUMN payment_method VARCHAR(50) DEFAULT 'Check' AFTER reference_number;
|
||||
1
db/migrations/20260322_add_avatar_to_users.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE users ADD COLUMN avatar VARCHAR(255) DEFAULT NULL;
|
||||
2
db/migrations/20260322_add_employee_to_nurses.sql
Normal 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;
|
||||
1
db/migrations/20260322_add_image_to_drugs.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE drugs ADD COLUMN image VARCHAR(255) NULL;
|
||||
1
db/migrations/20260322_add_nursing_notes_to_visits.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE visits ADD COLUMN IF NOT EXISTS nursing_notes TEXT AFTER temperature;
|
||||
55
db/migrations/20260322_create_hr_module.sql
Normal 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
|
||||
);
|
||||
28
db/migrations/20260322_fix_appointments_start_time.sql
Normal 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;
|
||||
64
db/migrations/20260322_merge_doctors_nurses_into_hr.sql
Normal 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;
|
||||
@ -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;
|
||||
58
db/migrations/20260322_merge_doctors_nurses_into_hr_fix.sql
Normal 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;
|
||||
1
db/migrations/20260323_add_active_to_departments.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE departments ADD COLUMN active BOOLEAN DEFAULT 1;
|
||||
1
db/migrations/20260323_add_room_number_to_employees.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE employees ADD COLUMN room_number VARCHAR(50) NULL AFTER email;
|
||||
3
db/migrations/20260328_add_smart_queue_routing.sql
Normal 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;
|
||||
12
db/migrations/20260328_z_seed_employees_if_empty.sql
Normal 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');
|
||||
53
db/migrations/20260329_add_sick_leave_to_visits.sql
Normal file
@ -0,0 +1,53 @@
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'visits';
|
||||
|
||||
-- Add sick_leave_days
|
||||
SET @columnname = 'sick_leave_days';
|
||||
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",
|
||||
CONCAT("ALTER TABLE ", @tablename, " ADD COLUMN ", @columnname, " INT NULL;")
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
|
||||
-- Add sick_leave_start_date
|
||||
SET @columnname = 'sick_leave_start_date';
|
||||
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",
|
||||
CONCAT("ALTER TABLE ", @tablename, " ADD COLUMN ", @columnname, " DATE NULL;")
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
|
||||
-- Add sick_leave_remarks
|
||||
SET @columnname = 'sick_leave_remarks';
|
||||
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",
|
||||
CONCAT("ALTER TABLE ", @tablename, " ADD COLUMN ", @columnname, " TEXT NULL;")
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
1
db/migrations/20260329_change_payment_method_type.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE bills MODIFY COLUMN payment_method VARCHAR(50) DEFAULT 'Cash';
|
||||
37
debug_queue.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
$pdo = db();
|
||||
|
||||
echo "<h2>Patient Queue (Latest 5)</h2>";
|
||||
$stmt = $pdo->query("SELECT * FROM patient_queue ORDER BY id DESC LIMIT 5");
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($rows) {
|
||||
echo "<table border='1'><tr>";
|
||||
foreach (array_keys($rows[0]) as $k) echo "<th>$k</th>";
|
||||
echo "</tr>";
|
||||
foreach ($rows as $row) {
|
||||
echo "<tr>";
|
||||
foreach ($row as $v) echo "<td>$v</td>";
|
||||
echo "</tr>";
|
||||
}
|
||||
echo "</table>";
|
||||
} else {
|
||||
echo "No records in patient_queue.<br>";
|
||||
}
|
||||
|
||||
echo "<h2>Visits (Latest 5)</h2>";
|
||||
$stmt = $pdo->query("SELECT id, patient_id, doctor_id, created_at FROM visits ORDER BY id DESC LIMIT 5");
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($rows) {
|
||||
echo "<table border='1'><tr>";
|
||||
foreach (array_keys($rows[0]) as $k) echo "<th>$k</th>";
|
||||
echo "</tr>";
|
||||
foreach ($rows as $row) {
|
||||
echo "<tr>";
|
||||
foreach ($row as $v) echo "<td>$v</td>";
|
||||
echo "</tr>";
|
||||
}
|
||||
echo "</table>";
|
||||
} else {
|
||||
echo "No records in visits.<br>";
|
||||
}
|
||||
10
delete_test_appointment.php
Normal 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";
|
||||
|
||||
12
delete_test_appointment_cleanup.php
Normal 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
@ -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';
|
||||
35
download_patient_template.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header("Location: login.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="patients_import_template.csv"');
|
||||
|
||||
// Output UTF-8 BOM for Excel
|
||||
echo "\xEF\xBB\xBF";
|
||||
|
||||
$output = fopen('php://output', 'w');
|
||||
|
||||
// Header row
|
||||
fputcsv($output, [
|
||||
'Patient Name',
|
||||
'DOB',
|
||||
'Nationality',
|
||||
'Telephone',
|
||||
'City'
|
||||
]);
|
||||
|
||||
// Sample row
|
||||
fputcsv($output, [
|
||||
'John Doe',
|
||||
'1990-01-01',
|
||||
'Omani',
|
||||
'96812345678',
|
||||
'Muscat'
|
||||
]);
|
||||
|
||||
fclose($output);
|
||||
exit;
|
||||
22
drugs.php
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
1307
includes/actions.php
Normal file
90
includes/auth.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
function check_auth() {
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header("Location: login.php");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function current_user() {
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
global $db;
|
||||
if (!isset($db)) {
|
||||
$db = db();
|
||||
}
|
||||
|
||||
// Check cache first (optional, but good for performance)
|
||||
if (isset($_SESSION['user_cache']) && $_SESSION['user_cache']['id'] == $_SESSION['user_id']) {
|
||||
return $_SESSION['user_cache'];
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("
|
||||
SELECT u.*, r.slug as role_slug, r.permissions
|
||||
FROM users u
|
||||
JOIN roles r ON u.role_id = r.id
|
||||
WHERE u.id = ? AND u.active = 1
|
||||
");
|
||||
$stmt->execute([$_SESSION['user_id']]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($user) {
|
||||
$_SESSION['user_cache'] = $user;
|
||||
return $user;
|
||||
}
|
||||
|
||||
// User not found or inactive, logout
|
||||
session_destroy();
|
||||
header("Location: login.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
function has_role($role_slug) {
|
||||
$user = current_user();
|
||||
if (!$user) return false;
|
||||
|
||||
if ($user['role_slug'] === 'admin') return true; // Admin has all roles
|
||||
|
||||
return $user['role_slug'] === $role_slug;
|
||||
}
|
||||
|
||||
function has_permission($permission) {
|
||||
$user = current_user();
|
||||
if (!$user) return false;
|
||||
|
||||
if ($user['role_slug'] === 'admin') return true; // Admin has all permissions
|
||||
|
||||
// Decode permissions JSON
|
||||
$perms = json_decode($user['permissions'], true);
|
||||
if (!$perms) return false;
|
||||
|
||||
if (in_array('*', $perms)) return true;
|
||||
|
||||
return in_array($permission, $perms);
|
||||
}
|
||||
|
||||
function require_role($role_slug) {
|
||||
if (!has_role($role_slug)) {
|
||||
http_response_code(403);
|
||||
die("Access Denied: You do not have the required role.");
|
||||
}
|
||||
}
|
||||
|
||||
function require_permission($permission) {
|
||||
if (!has_permission($permission)) {
|
||||
http_response_code(403);
|
||||
die("Access Denied: You do not have the required permission: " . htmlspecialchars($permission));
|
||||
}
|
||||
}
|
||||
|
||||
function is_admin() {
|
||||
return has_role('admin');
|
||||
}
|
||||
44
includes/common_data.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
// Common data for selects
|
||||
$all_doctors = $db->query("
|
||||
SELECT e.id, e.name_$lang as name
|
||||
FROM employees e
|
||||
JOIN positions p ON e.position_id = p.id
|
||||
WHERE UPPER(p.name_en) = 'DOCTOR'
|
||||
")->fetchAll();
|
||||
|
||||
$all_nurses = $db->query("
|
||||
SELECT e.id, e.name_$lang as name
|
||||
FROM employees e
|
||||
JOIN positions p ON e.position_id = p.id
|
||||
WHERE UPPER(p.name_en) = 'NURSE'
|
||||
")->fetchAll();
|
||||
|
||||
$all_patients = $db->query("SELECT id, name, phone, civil_id, dob, gender, address FROM patients")->fetchAll();
|
||||
$all_departments = $db->query("SELECT id, name_$lang as name FROM departments")->fetchAll();
|
||||
$all_employees = $db->query("SELECT id, name_$lang as name FROM employees")->fetchAll();
|
||||
$all_positions = $db->query("SELECT id, name_$lang as name FROM positions")->fetchAll();
|
||||
$all_insurance = $db->query("SELECT id, name_$lang as name FROM insurance_companies")->fetchAll();
|
||||
$all_test_groups = $db->query("SELECT id, name_$lang as name FROM test_groups")->fetchAll();
|
||||
$all_tests = $db->query("SELECT id, name_$lang as name, price, normal_range FROM laboratory_tests")->fetchAll();
|
||||
$all_services = $db->query("SELECT id, name_$lang as name, price FROM services WHERE is_active = 1")->fetchAll();
|
||||
|
||||
// X-Ray Data
|
||||
$all_xray_groups = $db->query("SELECT id, name_$lang as name FROM xray_groups")->fetchAll();
|
||||
$all_xrays = $db->query("SELECT id, name_$lang as name, price FROM xray_tests")->fetchAll();
|
||||
|
||||
// Drugs Data
|
||||
$all_drugs = $db->query("SELECT id, name_$lang as name, default_dosage, default_instructions, price FROM drugs")->fetchAll();
|
||||
|
||||
$scheduled_appointments = $db->query("
|
||||
SELECT a.id, p.name as patient_name, a.start_time, a.patient_id, a.doctor_id,
|
||||
e.name_$lang as doctor_name
|
||||
FROM appointments a
|
||||
JOIN patients p ON a.patient_id = p.id
|
||||
LEFT JOIN employees e ON a.doctor_id = e.id
|
||||
WHERE a.status = 'Scheduled'
|
||||
ORDER BY a.start_time ASC")->fetchAll();
|
||||
|
||||
$all_countries = require __DIR__ . "/countries.php";
|
||||
|
||||
$all_cities = $db->query("SELECT id, name_$lang as name FROM cities")->fetchAll();
|
||||
28
includes/countries.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
// includes/countries.php
|
||||
return [
|
||||
"Afghanistan", "Albania", "Algeria", "Andorra", "Angola", "Antigua and Barbuda", "Argentina", "Armenia", "Australia", "Austria", "Azerbaijan",
|
||||
"Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bhutan", "Bolivia", "Bosnia and Herzegovina", "Botswana", "Brazil", "Brunei", "Bulgaria", "Burkina Faso", "Burundi",
|
||||
"Cabo Verde", "Cambodia", "Cameroon", "Canada", "Central African Republic", "Chad", "Chile", "China", "Colombia", "Comoros", "Congo (Congo-Brazzaville)", "Costa Rica", "Croatia", "Cuba", "Cyprus", "Czechia (Czech Republic)",
|
||||
"Democratic Republic of the Congo", "Denmark", "Djibouti", "Dominica", "Dominican Republic",
|
||||
"Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Eswatini (fmr. 'Swaziland')", "Ethiopia",
|
||||
"Fiji", "Finland", "France",
|
||||
"Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Greece", "Grenada", "Guatemala", "Guinea", "Guinea-Bissau", "Guyana",
|
||||
"Haiti", "Holy See", "Honduras", "Hungary",
|
||||
"Iceland", "India", "Indonesia", "Iran", "Iraq", "Ireland", "Israel", "Italy", "Ivory Coast",
|
||||
"Jamaica", "Japan", "Jordan",
|
||||
"Kazakhstan", "Kenya", "Kiribati", "Kuwait", "Kyrgyzstan",
|
||||
"Laos", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg",
|
||||
"Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Mauritania", "Mauritius", "Mexico", "Micronesia", "Moldova", "Monaco", "Mongolia", "Montenegro", "Morocco", "Mozambique", "Myanmar (formerly Burma)",
|
||||
"Namibia", "Nauru", "Nepal", "Netherlands", "New Zealand", "Nicaragua", "Niger", "Nigeria", "North Korea", "North Macedonia", "Norway",
|
||||
"Oman",
|
||||
"Pakistan", "Palau", "Palestine State", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Poland", "Portugal",
|
||||
"Qatar",
|
||||
"Romania", "Russia", "Rwanda",
|
||||
"Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Korea", "South Sudan", "Spain", "Sri Lanka", "Sudan", "Suriname", "Sweden", "Switzerland", "Syria",
|
||||
"Tajikistan", "Tanzania", "Thailand", "Timor-Leste", "Togo", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Tuvalu",
|
||||
"Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States of America", "Uruguay", "Uzbekistan",
|
||||
"Vanuatu", "Venezuela", "Vietnam",
|
||||
"Yemen",
|
||||
"Zambia", "Zimbabwe"
|
||||
];
|
||||